Files
personal-toolbox/tests/PersonalToolbox.Tests/Program.cs
home-PC bbc183cef6 test: 增加工具箱本地验证覆盖
新增不依赖外部 NuGet 的测试项目,覆盖快捷键解析、组合校验、路径和网址校验,以及配置读写、导出和数据版本策略。

同步维护 development.md,记录本轮完成情况、后续可选打磨点和最新验证命令。
2026-05-27 14:59:07 +08:00

192 lines
6.9 KiB
C#

using System.IO.Compression;
using System.IO;
using System.Windows.Input;
using PersonalToolbox.Models;
using PersonalToolbox.Services;
var tests = new (string Name, Func<Task> Run)[]
{
("快捷键解析和捕获格式化", TestHotkeys),
("组合校验覆盖循环、重复和缺失引用", TestCombinationValidation),
("路径和网址校验", TestPathValidation),
("配置读写、导出和数据版本校验", TestConfiguration)
};
var failed = 0;
foreach (var test in tests)
{
try
{
await test.Run();
Console.WriteLine($"PASS {test.Name}");
}
catch (Exception ex)
{
failed++;
Console.WriteLine($"FAIL {test.Name}");
Console.WriteLine(ex);
}
}
if (failed > 0)
{
Environment.Exit(1);
}
static Task TestHotkeys()
{
Assert(HotkeyParser.TryParse("Ctrl + Alt + T", out _, out _), "Ctrl + Alt + T 应可解析");
Assert(HotkeyParser.Normalize("alt + ctrl + t") == "Ctrl+Alt+T", "快捷键应按固定顺序归一化");
Assert(!HotkeyParser.TryParse("T", out _, out _), "纯单键快捷键不应被接受");
Assert(HotkeyParser.TryFormatFromInput(ModifierKeys.Control | ModifierKeys.Shift, Key.D, out var hotkey), "捕获输入应成功");
Assert(hotkey == "Ctrl+Shift+D", "捕获输入应生成规范文本");
return Task.CompletedTask;
}
static Task TestCombinationValidation()
{
var launcher = new ToolLaunchService();
var toolA = CreateTool("tool-a", "工具 A");
var toolB = CreateTool("tool-b", "工具 B");
var child = CreateCombination("combo-child", "子组合", "tool-b");
var parent = CreateCombination("combo-parent", "父组合", "tool-a", "combo-child");
var valid = launcher.ValidateCombination(parent, [toolA, toolB, child, parent]);
Assert(valid.Success, "不重复的嵌套组合应通过校验");
child.Combination!.Members.Add(new CombinationMember { ToolId = "tool-a", SortOrder = 1 });
var duplicate = launcher.ValidateCombination(parent, [toolA, toolB, child, parent]);
Assert(!duplicate.Success && duplicate.Kind == LaunchResultKind.DuplicateTool, "展开后重复工具应被拦截");
child.Combination.Members.Clear();
child.Combination.Members.Add(new CombinationMember { ToolId = "combo-parent" });
var circular = launcher.ValidateCombination(parent, [toolA, child, parent]);
Assert(!circular.Success && circular.Kind == LaunchResultKind.CircularReference, "循环引用应被拦截");
child.Combination.Members.Clear();
child.Combination.Members.Add(new CombinationMember { ToolId = "missing-tool" });
var missing = launcher.ValidateCombination(parent, [toolA, child, parent]);
Assert(!missing.Success && missing.Kind == LaunchResultKind.MissingReference, "缺失成员引用应被拦截");
return Task.CompletedTask;
}
static Task TestPathValidation()
{
Assert(PathValidationService.IsValidUrl("example.com", out var url), "无协议网址应自动补全");
Assert(url.StartsWith("https://example.com", StringComparison.OrdinalIgnoreCase), "网址应补全为 https");
Assert(!PathValidationService.IsValidUrl("file://c:/test.txt", out _), "第一版只接受 http/https");
var tool = CreateTool("local-missing", "缺失工具");
tool.LaunchTarget = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "missing.exe");
var report = new PathValidationService().ValidateTools([tool]);
Assert(report.HasIssues, "缺失路径应生成检查问题");
Assert(tool.PathInvalid, "缺失路径应标记到工具模型");
return Task.CompletedTask;
}
static async Task TestConfiguration()
{
var root = Path.Combine(Path.GetTempPath(), "PersonalToolboxTests_" + Guid.NewGuid().ToString("N"));
var service = new ConfigurationService(root);
var data = new ToolboxData
{
Settings = new AppSettings
{
Theme = "Dark",
CardSize = "Large",
UiScale = 1.2,
ShowToolDescriptions = false
},
Categories =
[
new CategoryItem { Id = "category-test", Name = "测试分类", IconKey = "dev", SortOrder = 0 }
],
Tools =
[
CreateTool("tool-config", "配置工具")
],
AutoRunEntries = []
};
data.Tools[0].CategoryId = "category-test";
await service.SaveAsync(data);
var loaded = await service.LoadAsync();
Assert(loaded.Settings.DataVersion == ConfigurationService.CurrentDataVersion, "加载后应升级到当前数据版本");
Assert(loaded.Settings.Theme == "Dark", "设置应完成往返保存");
Assert(loaded.Tools.Any(tool => tool.Id == "tool-config"), "工具应完成往返保存");
var backupDirectory = Path.Combine(root, "backups", "should-not-export");
Directory.CreateDirectory(backupDirectory);
await File.WriteAllTextAsync(Path.Combine(backupDirectory, "ignored.txt"), "ignored");
var zipPath = Path.Combine(Path.GetTempPath(), "PersonalToolboxExport_" + Guid.NewGuid().ToString("N") + ".zip");
await service.ExportAsync(zipPath, loaded);
using (var archive = ZipFile.OpenRead(zipPath))
{
Assert(archive.GetEntry("appsettings.json") is not null, "导出应包含 appsettings.json");
Assert(archive.Entries.All(entry => !entry.FullName.StartsWith("backups/", StringComparison.OrdinalIgnoreCase)), "导出不应包含 backups 目录");
}
var futureRoot = Path.Combine(Path.GetTempPath(), "PersonalToolboxTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(futureRoot);
await File.WriteAllTextAsync(Path.Combine(futureRoot, "appsettings.json"), "{\"dataVersion\":999}");
var futureService = new ConfigurationService(futureRoot);
await AssertThrowsAsync<InvalidDataException>(() => futureService.LoadAsync(), "高版本数据应被拒绝");
}
static ToolItem CreateTool(string id, string name)
{
return new ToolItem
{
Id = id,
Name = name,
Type = ToolType.Local,
CategoryId = "category-test",
LaunchTarget = Environment.ProcessPath ?? "dotnet",
IconKey = "local"
};
}
static ToolItem CreateCombination(string id, string name, params string[] members)
{
return new ToolItem
{
Id = id,
Name = name,
Type = ToolType.Combination,
CategoryId = "category-test",
IconKey = "combination",
Combination = new CombinationConfig
{
Members = members.Select((toolId, index) => new CombinationMember
{
ToolId = toolId,
SortOrder = index
}).ToList()
}
};
}
static async Task AssertThrowsAsync<TException>(Func<Task> action, string message)
where TException : Exception
{
try
{
await action();
}
catch (TException)
{
return;
}
throw new InvalidOperationException(message);
}
static void Assert(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}