test: 添加 CLI 命令单元测试
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
@@ -7,24 +7,65 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer.Cor
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer", "src\Gpulse.WCT.DataAnalyzer\Gpulse.WCT.DataAnalyzer.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345603}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer.Tests", "tests\Gpulse.WCT.DataAnalyzer.Tests\Gpulse.WCT.DataAnalyzer.Tests.csproj", "{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A1B2C3D4-1234-5678-9ABC-DEF012345600}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -48,6 +48,21 @@
|
||||
| ORM | Entity Framework Core | 8.0 |
|
||||
| 数据库 | SQLite / PostgreSQL | - |
|
||||
| CLI | System.CommandLine | 2.0-beta4 |
|
||||
| 测试 | NUnit | 4.2 |
|
||||
|
||||
---
|
||||
|
||||
## 测试
|
||||
|
||||
CLI 单元测试项目位于 `tests/Gpulse.WCT.DataAnalyzer.Tests`,使用 NUnit + 内存 SQLite:
|
||||
|
||||
- 直接实例化 `ParseCommand` / `AggregateCommand` / `ExportCommand` / `CleanCommand` / `AnalyzeCommand`(构造方式与 `Program.Main` 一致),验证命令绑定、参数解析与输出。
|
||||
- 每个测试独立的「本地库/发布库」内存连接 + 临时日志 fixture 目录,互不污染。
|
||||
- 不启动完整 Host/Serilog,仅覆盖 CLI 命令层行为。
|
||||
|
||||
```bash
|
||||
dotnet test Gpulse.WCT.DataAnalyzer.sln
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
53
tests/Gpulse.WCT.DataAnalyzer.Tests/CliStructureTests.cs
Normal file
53
tests/Gpulse.WCT.DataAnalyzer.Tests/CliStructureTests.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Gpulse.WCT.DataAnalyzer.Commands;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.CommandLine;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class CliStructureTests : CommandTestBase
|
||||
{
|
||||
/// <summary>按 Program.Main 的方式组装根命令。</summary>
|
||||
private RootCommand BuildRoot()
|
||||
{
|
||||
var root = new RootCommand("Gpulse.WCT.DataAnalyzer - FOD Log Data Parser");
|
||||
root.AddCommand(new ParseCommand(Host.Services.GetRequiredService<ParseService>()));
|
||||
root.AddCommand(new AggregateCommand(Host.Services.GetRequiredService<AggregationService>()));
|
||||
root.AddCommand(new ExportCommand(Host.Services.GetRequiredService<ExportService>()));
|
||||
root.AddCommand(new CleanCommand(Host.Services.GetRequiredService<CleanService>()));
|
||||
root.AddCommand(new AnalyzeCommand(
|
||||
Host.Services.GetRequiredService<PlossAnalysisService>(),
|
||||
Host.Services.GetRequiredService<QfodCalibrationService>()));
|
||||
return root;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Help_ListsAllSubcommands()
|
||||
{
|
||||
var (code, stdout, _) = await BuildRoot().RunCaptureAsync("--help");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("parse"));
|
||||
Assert.That(stdout, Does.Contain("aggregate"));
|
||||
Assert.That(stdout, Does.Contain("export"));
|
||||
Assert.That(stdout, Does.Contain("clean"));
|
||||
Assert.That(stdout, Does.Contain("analyze"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UnknownCommand_ReturnsError()
|
||||
{
|
||||
var (code, stdout, stderr) = await BuildRoot().RunCaptureAsync("nosuchcommand");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(1));
|
||||
Assert.That(stdout + stderr, Does.Contain("nosuchcommand"));
|
||||
});
|
||||
}
|
||||
}
|
||||
38
tests/Gpulse.WCT.DataAnalyzer.Tests/CliTestExtensions.cs
Normal file
38
tests/Gpulse.WCT.DataAnalyzer.Tests/CliTestExtensions.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.CommandLine;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 命令执行辅助:运行命令并捕获标准输出/标准错误。
|
||||
/// 命令 handler 直接使用 Console.WriteLine,因此重定向 System.Console 即可捕获;
|
||||
/// System.CommandLine 自身的帮助/校验输出也会经默认控制台写入同一流。
|
||||
/// </summary>
|
||||
public static class CliTestExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行命令并返回 (退出码, 标准输出, 标准错误)。
|
||||
/// </summary>
|
||||
public static async Task<(int ExitCode, string StdOut, string StdErr)> RunCaptureAsync(
|
||||
this Command command,
|
||||
params string[] args)
|
||||
{
|
||||
var originalOut = Console.Out;
|
||||
var originalError = Console.Error;
|
||||
|
||||
using var outWriter = new StringWriter();
|
||||
using var errWriter = new StringWriter();
|
||||
|
||||
Console.SetOut(outWriter);
|
||||
Console.SetError(errWriter);
|
||||
try
|
||||
{
|
||||
var exitCode = await command.InvokeAsync(args);
|
||||
return (exitCode, outWriter.ToString(), errWriter.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
Console.SetError(originalError);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
tests/Gpulse.WCT.DataAnalyzer.Tests/CliTestHost.cs
Normal file
101
tests/Gpulse.WCT.DataAnalyzer.Tests/CliTestHost.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// CLI 测试宿主:用内存 SQLite 搭建与 Program.Main 一致的服务容器。
|
||||
/// 每个测试实例持有独立的「本地库」/「发布库」连接与一个临时目录,互不污染。
|
||||
/// </summary>
|
||||
public sealed class CliTestHost : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _localConnection;
|
||||
private readonly SqliteConnection _releaseConnection;
|
||||
private readonly ServiceProvider _provider;
|
||||
private readonly IServiceScope _scope;
|
||||
|
||||
/// <summary>测试作用域的服务提供者(与命令执行共用同一作用域)。</summary>
|
||||
public IServiceProvider Services => _scope.ServiceProvider;
|
||||
|
||||
/// <summary>每个测试独立的临时目录,用于放置日志 fixture 文件。</summary>
|
||||
public string TempDir { get; }
|
||||
|
||||
public WctMinerDbContext LocalDb => Services.GetRequiredService<WctMinerDbContext>();
|
||||
public ReleaseDbContext ReleaseDb => Services.GetRequiredService<ReleaseDbContext>();
|
||||
|
||||
private CliTestHost(
|
||||
SqliteConnection localConnection,
|
||||
SqliteConnection releaseConnection,
|
||||
ServiceProvider provider,
|
||||
IServiceScope scope,
|
||||
string tempDir)
|
||||
{
|
||||
_localConnection = localConnection;
|
||||
_releaseConnection = releaseConnection;
|
||||
_provider = provider;
|
||||
_scope = scope;
|
||||
TempDir = tempDir;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建测试宿主:本地库/发布库各用一条独立的内存 SQLite 连接(镜像生产环境的两个库文件),
|
||||
/// 并做 EnsureCreated 与种子数据初始化。
|
||||
/// </summary>
|
||||
public static async Task<CliTestHost> CreateAsync()
|
||||
{
|
||||
var tempDir = Directory.CreateTempSubdirectory("wct-tests-").FullName;
|
||||
|
||||
// 两个库各自独立连接,避免共享连接时 EnsureCreated 相互干扰
|
||||
var localConnection = new SqliteConnection("Data Source=:memory:");
|
||||
await localConnection.OpenAsync();
|
||||
|
||||
var releaseConnection = new SqliteConnection("Data Source=:memory:");
|
||||
await releaseConnection.OpenAsync();
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Database:Type"] = "sqlite",
|
||||
["Parser:BatchSize"] = "1000",
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddDbContext<WctMinerDbContext>(o => o.UseSqlite(localConnection));
|
||||
services.AddDbContext<ReleaseDbContext>(o => o.UseSqlite(releaseConnection));
|
||||
services.AddApplicationServices();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// 初始化表结构与种子数据(与 Program.Main 一致)
|
||||
using (var scope = provider.CreateScope())
|
||||
{
|
||||
var local = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
||||
await local.Database.EnsureCreatedAsync();
|
||||
SeedData.Initialize(local);
|
||||
|
||||
var release = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
|
||||
await release.Database.EnsureCreatedAsync();
|
||||
}
|
||||
|
||||
var hostScope = provider.CreateScope();
|
||||
return new CliTestHost(localConnection, releaseConnection, provider, hostScope, tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_scope.Dispose();
|
||||
_provider.Dispose();
|
||||
_localConnection.Dispose();
|
||||
_releaseConnection.Dispose();
|
||||
|
||||
try { Directory.Delete(TempDir, recursive: true); }
|
||||
catch { /* 尽力清理临时目录 */ }
|
||||
}
|
||||
}
|
||||
37
tests/Gpulse.WCT.DataAnalyzer.Tests/CommandTestBase.cs
Normal file
37
tests/Gpulse.WCT.DataAnalyzer.Tests/CommandTestBase.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Gpulse.WCT.DataAnalyzer.Commands;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// CLI 命令测试基类:每个测试一个独立的内存数据库 + 临时目录。
|
||||
/// </summary>
|
||||
public abstract class CommandTestBase
|
||||
{
|
||||
protected CliTestHost Host { get; private set; } = null!;
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUp() => Host = await CliTestHost.CreateAsync();
|
||||
|
||||
[TearDown]
|
||||
public void TearDown() => Host.Dispose();
|
||||
|
||||
// 命令构造方式与 Program.Main 一致:从 DI 解析服务再构造命令
|
||||
protected ParseCommand Parse() => new(Host.Services.GetRequiredService<ParseService>());
|
||||
|
||||
protected AggregateCommand Aggregate() => new(Host.Services.GetRequiredService<AggregationService>());
|
||||
|
||||
protected ExportCommand Export() => new(Host.Services.GetRequiredService<ExportService>());
|
||||
|
||||
protected CleanCommand Clean() => new(Host.Services.GetRequiredService<CleanService>());
|
||||
|
||||
protected AnalyzeCommand Analyze() => new(
|
||||
Host.Services.GetRequiredService<PlossAnalysisService>(),
|
||||
Host.Services.GetRequiredService<QfodCalibrationService>());
|
||||
|
||||
/// <summary>在测试临时目录下新建一个子目录。</summary>
|
||||
protected string NewDir() =>
|
||||
Directory.CreateDirectory(Path.Combine(Host.TempDir, Guid.NewGuid().ToString("N"))).FullName;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public class AggregateCommandTests : CommandTestBase
|
||||
{
|
||||
[Test]
|
||||
public async Task Aggregate_EmptyDatabase_ReportsZero()
|
||||
{
|
||||
var (code, stdout, _) = await Aggregate().RunCaptureAsync();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Aggregated 0 local Ploss records into 0 release records."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Aggregate_AfterParse_GroupsByThresholdIntoReleaseDb()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
|
||||
new TestFixtures.PlossTwoLineRow(Field7: 100, Field9: 350, Field10: 350),
|
||||
new TestFixtures.PlossTwoLineRow(Field7: 110, Field9: 350, Field10: 350),
|
||||
new TestFixtures.PlossTwoLineRow(Field7: 200, Field9: 500, Field10: 500));
|
||||
await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
var (code, stdout, _) = await Aggregate().RunCaptureAsync();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Aggregated 3 local Ploss records into 1 release records."));
|
||||
});
|
||||
|
||||
var record = await Host.ReleaseDb.ChargingParameters.SingleAsync();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// 维度来自文件名:singleMold(TxPanel) / v1.0(TxHardware) / hex2_1(TxSoftware) / iPhone15(RxType)
|
||||
Assert.That(record.CarFactory, Is.EqualTo("singleMold"));
|
||||
Assert.That(record.CarModel, Is.EqualTo("v1.0"));
|
||||
Assert.That(record.PhoneModel, Is.EqualTo("iPhone15"));
|
||||
// Field9=350 的两行 Field7 取平均 = (100+110)/2
|
||||
Assert.That(record.Power350mW, Is.EqualTo(105));
|
||||
Assert.That(record.Power500mW, Is.EqualTo(200));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Aggregate_DefaultRebuild_DoesNotDuplicateOnSecondRun()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
|
||||
new TestFixtures.PlossTwoLineRow(Field7: 100, Field9: 350, Field10: 350));
|
||||
await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
await Aggregate().RunCaptureAsync();
|
||||
await Aggregate().RunCaptureAsync();
|
||||
|
||||
Assert.That(await Host.ReleaseDb.ChargingParameters.CountAsync(), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Aggregate_RebuildFalse_InsertsIntoEmptyTable()
|
||||
{
|
||||
// 注:发布库对业务键 (车厂/车型/手机厂商/型号) 有唯一索引;
|
||||
// append 模式会对全部本地记录重新入库,因此只适合表为空时使用,
|
||||
// 对已聚合过的同一数据再 append 会因唯一索引冲突而失败(聚合命令捕获并输出 "Aggregation failed")。
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
|
||||
new TestFixtures.PlossTwoLineRow(Field7: 100, Field9: 350, Field10: 350));
|
||||
await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
var (code, stdout, _) = await Aggregate().RunCaptureAsync("--rebuild", "false");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Aggregated 1 local Ploss records into 1 release records."));
|
||||
});
|
||||
Assert.That(await Host.ReleaseDb.ChargingParameters.CountAsync(), Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public class AnalyzeCommandTests : CommandTestBase
|
||||
{
|
||||
[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",
|
||||
(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_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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public class CleanCommandTests : CommandTestBase
|
||||
{
|
||||
[Test]
|
||||
public async Task Clean_NoConfirm_PrintsPrompt()
|
||||
{
|
||||
var (code, stdout, _) = await Clean().RunCaptureAsync("--all");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Please add --confirm to confirm the clean operation."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Clean_InvalidScenarioGuid_PrintsMessage()
|
||||
{
|
||||
var (_, stdout, _) = await Clean().RunCaptureAsync("--scenario", "not-a-guid", "--confirm");
|
||||
|
||||
Assert.That(stdout, Does.Contain("Invalid scenario ID: not-a-guid"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Clean_Scenario_NotExist_PrintsZeroCount()
|
||||
{
|
||||
var scenarioId = Guid.NewGuid();
|
||||
var (_, stdout, _) = await Clean().RunCaptureAsync("--scenario", scenarioId.ToString(), "--confirm");
|
||||
|
||||
Assert.That(stdout, Does.Contain($"Cleaned 0 records for scenario {scenarioId}"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Clean_Scenario_WithConfirm_SoftDeletesRelatedRecords()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
|
||||
new TestFixtures.PlossTwoLineRow(),
|
||||
new TestFixtures.PlossTwoLineRow(Field9: 2500, Field12: 1));
|
||||
await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
var scenarioId = await Host.LocalDb.TestScenarios.Select(s => s.Id).SingleAsync();
|
||||
|
||||
var (code, stdout, _) = await Clean().RunCaptureAsync("--scenario", scenarioId.ToString(), "--confirm");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain($"Cleaned 3 records for scenario {scenarioId}"));
|
||||
});
|
||||
|
||||
// 2 条 Ploss + 1 个场景全部软删除
|
||||
Assert.That(await Host.LocalDb.PlossRecords.IgnoreQueryFilters().CountAsync(r => r.IsDeleted), Is.EqualTo(2));
|
||||
Assert.That(await Host.LocalDb.TestScenarios.IgnoreQueryFilters().CountAsync(s => s.IsDeleted), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Clean_All_WithConfirm_SoftDeletesEverything()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
|
||||
new TestFixtures.PlossTwoLineRow(),
|
||||
new TestFixtures.PlossTwoLineRow());
|
||||
await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
var (code, stdout, _) = await Clean().RunCaptureAsync("--all", "--confirm");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Cleaned 3 records in total"));
|
||||
});
|
||||
|
||||
Assert.That(await Host.LocalDb.PlossRecords.IgnoreQueryFilters().CountAsync(r => r.IsDeleted), Is.EqualTo(2));
|
||||
Assert.That(await Host.LocalDb.TestScenarios.IgnoreQueryFilters().CountAsync(s => s.IsDeleted), Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public class ExportCommandTests : CommandTestBase
|
||||
{
|
||||
private static string ResolveOutputDir(params string[] segments) =>
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, string.Join(Path.DirectorySeparatorChar, segments));
|
||||
|
||||
[Test]
|
||||
public async Task Export_ChargingParametersCsv_WritesFile()
|
||||
{
|
||||
Host.ReleaseDb.ChargingParameters.Add(new ChargingParameterRecord
|
||||
{
|
||||
CarFactory = "TestFactory",
|
||||
CarModel = "ModelX",
|
||||
PhoneBrand = "Apple",
|
||||
PhoneModel = "iPhone15",
|
||||
Power350mW = 100,
|
||||
Power500mW = 200,
|
||||
});
|
||||
await Host.ReleaseDb.SaveChangesAsync();
|
||||
|
||||
var (code, stdout, _) = await Export().RunCaptureAsync(
|
||||
"--format", "csv",
|
||||
"--type", "charging-parameters",
|
||||
"--output", "data/output/export_cp");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("ChargingParameterDatabase CSV exported"));
|
||||
});
|
||||
|
||||
var file = Path.Combine(ResolveOutputDir("data", "output", "export_cp"), "WCT-ChargingParameterDatabase.csv");
|
||||
Assert.That(File.Exists(file), Is.True);
|
||||
|
||||
var content = await File.ReadAllTextAsync(file);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(content, Does.Contain("车厂"));
|
||||
Assert.That(content, Does.Contain("TestFactory"));
|
||||
Assert.That(content, Does.Contain("iPhone15"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Export_QfodCsv_AfterParse_WritesFile()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WriteQfodLog(dir, TestFixtures.QfodFileName, 20, 25, 21);
|
||||
await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
// 导出文件名带时间戳,先清空输出目录避免跨运行残留文件
|
||||
var outDir = ResolveOutputDir("data", "output", "export_qfod");
|
||||
if (Directory.Exists(outDir))
|
||||
Directory.Delete(outDir, recursive: true);
|
||||
|
||||
var (code, stdout, _) = await Export().RunCaptureAsync(
|
||||
"--format", "csv",
|
||||
"--type", "qfod",
|
||||
"--output", "data/output/export_qfod");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Qfod CSV exported"));
|
||||
});
|
||||
|
||||
var exported = Directory.GetFiles(outDir, "qfod_export_*.csv").Single();
|
||||
var content = await File.ReadAllTextAsync(exported);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(content, Does.Contain("delta_q"));
|
||||
Assert.That(content, Does.Contain("iPhone15"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Export_UnknownFormat_PrintsMessage()
|
||||
{
|
||||
var (code, stdout, _) = await Export().RunCaptureAsync("--format", "xml");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Unknown format: xml. Use 'csv' or 'json'."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Export_PathTraversal_PrintsFailure()
|
||||
{
|
||||
var (code, stdout, _) = await Export().RunCaptureAsync(
|
||||
"--format", "csv",
|
||||
"--output", "../evil");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Export failed"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public class ParseCommandTests : CommandTestBase
|
||||
{
|
||||
[Test]
|
||||
public async Task Parse_File_Missing_ReturnsFailureReport()
|
||||
{
|
||||
var missing = Path.Combine(Host.TempDir, "not-exist.log");
|
||||
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", missing);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("not-exist.log"));
|
||||
Assert.That(stdout, Does.Contain("FAILED"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_File_WithBadFileName_ReportsFilenameError()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = Path.Combine(dir, "bad-name.log");
|
||||
await File.WriteAllTextAsync(file, "0#:Q 1->25 45.5 20.5 2 0\n");
|
||||
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("bad-name.log"));
|
||||
Assert.That(stdout, Does.Contain("文件名格式不匹配"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_File_QfodLog_StoresRecordsAndReportsCount()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WriteQfodLog(dir, TestFixtures.QfodFileName, 20, 25, 21, -8);
|
||||
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain(TestFixtures.QfodFileName));
|
||||
Assert.That(stdout, Does.Contain("Qfod: 4"));
|
||||
Assert.That(stdout, Does.Contain("Ploss: 0"));
|
||||
});
|
||||
|
||||
Assert.That(await Host.LocalDb.QfodRecords.CountAsync(), Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_File_PlossLog_StoresRecordsAndReportsCount()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
|
||||
new TestFixtures.PlossTwoLineRow(),
|
||||
new TestFixtures.PlossTwoLineRow(Field9: 2500, Field10: 3000, Field12: 1));
|
||||
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Qfod: 0"));
|
||||
Assert.That(stdout, Does.Contain("Ploss: 2"));
|
||||
});
|
||||
|
||||
Assert.That(await Host.LocalDb.PlossRecords.CountAsync(), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_File_WithForceOption_Accepted()
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WriteQfodLog(dir, TestFixtures.QfodFileName, 20, 25, 21);
|
||||
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file, "--force");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Qfod: 3"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_Dir_ReportsSummaryAndTotals()
|
||||
{
|
||||
var dir = NewDir();
|
||||
TestFixtures.WriteQfodLog(dir, TestFixtures.QfodFileName, 20, 25, 21);
|
||||
TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
|
||||
new TestFixtures.PlossTwoLineRow(),
|
||||
new TestFixtures.PlossTwoLineRow());
|
||||
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync("--dir", dir);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("2 files processed"));
|
||||
Assert.That(stdout, Does.Contain("Total: Qfod=3, Ploss=2, Errors=0"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_Dir_Recursive_IncludesSubfolders()
|
||||
{
|
||||
var root = NewDir();
|
||||
var sub = Directory.CreateDirectory(Path.Combine(root, "sub")).FullName;
|
||||
TestFixtures.WriteQfodLog(root, TestFixtures.QfodFileName, 20, 25);
|
||||
TestFixtures.WritePlossLog(sub, TestFixtures.PlossFileName, new TestFixtures.PlossTwoLineRow());
|
||||
|
||||
var (_, plain, _) = await Parse().RunCaptureAsync("--dir", root);
|
||||
Assert.That(plain, Does.Contain("1 files processed"));
|
||||
|
||||
var (_, recursive, _) = await Parse().RunCaptureAsync("--dir", root, "--recursive");
|
||||
Assert.That(recursive, Does.Contain("2 files processed"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_Dir_NotExists_ReturnsFailureReport()
|
||||
{
|
||||
var missing = Path.Combine(Host.TempDir, "no-such-dir");
|
||||
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync("--dir", missing);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("FAILED"));
|
||||
Assert.That(stdout, Does.Contain("目录不存在"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_NeitherFileNorDir_PrintsPrompt()
|
||||
{
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Please specify --file or --dir"));
|
||||
});
|
||||
}
|
||||
}
|
||||
1
tests/Gpulse.WCT.DataAnalyzer.Tests/GlobalUsings.cs
Normal file
1
tests/Gpulse.WCT.DataAnalyzer.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using NUnit.Framework;
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>Gpulse.WCT.DataAnalyzer.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 测试框架 -->
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
|
||||
<!-- 测试宿主依赖(Core 只引用 Abstractions,这里补具体实现) -->
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Gpulse.WCT.DataAnalyzer\Gpulse.WCT.DataAnalyzer.csproj" />
|
||||
<ProjectReference Include="..\..\src\Gpulse.WCT.DataAnalyzer.Core\Gpulse.WCT.DataAnalyzer.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
80
tests/Gpulse.WCT.DataAnalyzer.Tests/TestFixtures.cs
Normal file
80
tests/Gpulse.WCT.DataAnalyzer.Tests/TestFixtures.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 构造 CLI 测试用的日志 fixture 文件。文件名符合 FileNameParser 约定的
|
||||
/// `TX面板-TX硬件-TX软件-RX类型-测试目的-日期-序号` 格式。
|
||||
/// </summary>
|
||||
public static class TestFixtures
|
||||
{
|
||||
public const string PlossFileName = "singleMold-v1.0-hex2_1-iPhone15-ploss_test-20260710-1.log";
|
||||
public const string QfodFileName = "singleMold-v1.0-hex2_1-iPhone15-qfod_test-20260710-1.log";
|
||||
|
||||
/// <summary>
|
||||
/// 两行 Ploss 记录(header + FOD 16 字段)。
|
||||
/// 默认值让恒等式成立(Field5 - Field4 - PowLoss - ploss == DeltaP)且余量 > 2000。
|
||||
/// </summary>
|
||||
public sealed record PlossTwoLineRow(
|
||||
int PowLoss = 2200,
|
||||
int DeltaP = 2500,
|
||||
int Field1 = 1,
|
||||
int Field2 = 0,
|
||||
int Field3 = 0,
|
||||
int Field4 = 800, // RX power
|
||||
int Field5 = 6000, // TX power
|
||||
int Field6 = 0,
|
||||
int Field7 = 0,
|
||||
int Field8 = 0,
|
||||
int Field9 = 500, // ploss
|
||||
int Field10 = 3000, // threshold
|
||||
int Field11 = 0,
|
||||
int Field12 = 0, // FOD 标志
|
||||
int Field13 = 0,
|
||||
int Field14 = 0,
|
||||
int Field15 = 0,
|
||||
int Field16 = 0);
|
||||
|
||||
/// <summary>写两行格式 Ploss 日志文件。</summary>
|
||||
public static string WritePlossLog(string dir, string fileName, params PlossTwoLineRow[] rows)
|
||||
{
|
||||
var path = Path.Combine(dir, fileName);
|
||||
var lines = new List<string>(rows.Length * 2);
|
||||
foreach (var r in rows)
|
||||
{
|
||||
lines.Add($"pow_loss = {r.PowLoss}, delta_p = {r.DeltaP}");
|
||||
lines.Add(
|
||||
$"FOD-> {r.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}");
|
||||
}
|
||||
File.WriteAllLines(path, lines);
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>写旧单行格式 Ploss 日志(14 字段),仅指定 ploss/threshold/FOD 三个关键字段。</summary>
|
||||
public static string WriteLegacyPlossLog(string dir, string fileName, params (int Ploss, int Threshold, int Fod)[] rows)
|
||||
{
|
||||
var path = Path.Combine(dir, fileName);
|
||||
var lines = rows
|
||||
.Select(r => $"FOD-> 1 0 0 0 0 0 0 0 {r.Ploss} {r.Threshold} 0 {r.Fod} 0 0")
|
||||
.ToArray();
|
||||
File.WriteAllLines(path, lines);
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>写 Qfod 日志文件,每行 DeltaQ 一个。</summary>
|
||||
public static string WriteQfodLog(string dir, string fileName, params int[] deltaQs)
|
||||
{
|
||||
var path = Path.Combine(dir, fileName);
|
||||
var lines = deltaQs
|
||||
.Select((d, i) =>
|
||||
{
|
||||
var charger = i % 2;
|
||||
var coil = i % 3;
|
||||
var currentQ = 40 + d;
|
||||
return $"{charger}#:Q {coil}->{d} {currentQ}.0 20.0 0 0";
|
||||
})
|
||||
.ToArray();
|
||||
File.WriteAllLines(path, lines);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user