649 lines
30 KiB
C#
649 lines
30 KiB
C#
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
using Microsoft.Data.Sqlite;
|
|
using YMhut.Box.Core.App;
|
|
using YMhut.Box.Core.Startup;
|
|
|
|
namespace YMhut.Box.Tests;
|
|
|
|
[TestClass]
|
|
[DoNotParallelize]
|
|
public sealed class StartupCheckTests
|
|
{
|
|
[TestMethod]
|
|
public async Task FastPreflightPassesForPureLangInstallLayout()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-fast");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var store = new StartupCheckStore(paths);
|
|
var service = new InstallIntegrityCheckService(paths, store);
|
|
|
|
var report = await service.RunFastPreflightAsync();
|
|
|
|
Assert.AreEqual(StartupCheckKind.FastPreflight, report.Kind);
|
|
Assert.AreEqual(0, report.CriticalIssueCount);
|
|
Assert.IsTrue(report.Items.Any(item => item.Id == "lang:zh-CN" && item.Status == StartupCheckStatus.Passed));
|
|
Assert.IsTrue(report.Items.Any(item => item.Id == "lang:en-US" && item.Status == StartupCheckStatus.Passed));
|
|
Assert.IsFalse(Directory.Exists(Path.Combine(appRoot, "Runtime")));
|
|
StringAssert.StartsWith(store.DatabasePath, Path.Combine(appRoot, "Logs"));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task FastPreflightDoesNotBlockDevelopmentRootCultureLayout()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-dev-layout");
|
|
var installRoot = CreateDevelopmentInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
|
|
|
|
var report = await service.RunFastPreflightAsync();
|
|
var resourcesPri = report.Items.FirstOrDefault(item => item.Id == "file:resources.pri");
|
|
var zhCn = report.Items.FirstOrDefault(item => item.Id == "lang:zh-CN");
|
|
var enUs = report.Items.FirstOrDefault(item => item.Id == "lang:en-US");
|
|
|
|
Assert.AreEqual(0, report.CriticalIssueCount);
|
|
Assert.IsNotNull(resourcesPri);
|
|
Assert.AreEqual(StartupCheckStatus.Missing, resourcesPri.Status);
|
|
Assert.AreEqual(StartupCheckSeverity.Warning, resourcesPri.Severity);
|
|
Assert.IsNotNull(zhCn);
|
|
Assert.AreEqual(StartupCheckStatus.Passed, zhCn.Status);
|
|
Assert.IsNotNull(enUs);
|
|
Assert.AreEqual(StartupCheckStatus.Passed, enUs.Status);
|
|
Assert.IsFalse(report.Items.Any(item => item.Id.StartsWith("lang:root:", StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task FastPreflightAcceptsWinUiDevelopmentPriAndRootCultureSatellites()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-dev-pri");
|
|
var installRoot = CreateDevelopmentInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
workspace.CreateFile(["install", "YMhutBox.pri"], string.Empty);
|
|
workspace.CreateFile(["install", "fr-FR", "Microsoft.ui.xaml.dll.mui"], string.Empty);
|
|
workspace.CreateFile(["install", "ja-JP", "Microsoft.ui.xaml.dll.mui"], string.Empty);
|
|
workspace.CreateFile(["install", "Tools", "Sample", "tool.exe"], string.Empty);
|
|
workspace.CreateFile(["install", "Metadata", "tools.json"], """{ "tools": [] }""");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
|
|
|
|
var report = await service.RunFastPreflightAsync();
|
|
|
|
Assert.AreEqual(0, report.VisibleIssueCount);
|
|
Assert.IsTrue(report.Items.Any(item => item.Id == "manifest" && item.Status == StartupCheckStatus.Skipped));
|
|
Assert.IsTrue(report.Items.Any(item => item.Id == "file:resources.pri" && item.Status == StartupCheckStatus.Passed));
|
|
Assert.IsTrue(report.Items.Any(item => item.Id == "lang:root-culture-summary" && item.Status == StartupCheckStatus.Skipped));
|
|
Assert.IsFalse(report.Items.Any(item => item.Id.StartsWith("lang:root:", StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task CurrentHistoryNormalizesLegacyDevelopmentLayoutFalsePositives()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-legacy-false-positive");
|
|
var installRoot = CreateDevelopmentInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
workspace.CreateFile(["install", "YMhutBox.pri"], string.Empty);
|
|
workspace.CreateFile(["install", "fr-FR", "Microsoft.ui.xaml.dll.mui"], string.Empty);
|
|
workspace.CreateFile(["install", "Tools", "Sample", "tool.exe"], string.Empty);
|
|
workspace.CreateFile(["install", "Metadata", "tools.json"], """{ "tools": [] }""");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var store = new StartupCheckStore(paths);
|
|
var manifestIdentity = InstallRootContext.BuildManifestIdentity(installRoot);
|
|
var oldReport = StartupCheckReport.Create(
|
|
StartupCheckKind.FastPreflight,
|
|
new DateTimeOffset(2026, 6, 1, 8, 0, 0, TimeSpan.Zero),
|
|
installRoot,
|
|
manifestIdentity,
|
|
InstallRootContext.BuildInstallIdentity(installRoot, manifestIdentity),
|
|
$"installRoot={installRoot}; manifest={manifestIdentity}",
|
|
[
|
|
new StartupCheckItem("manifest", "安装清单", StartupCheckSeverity.Warning, StartupCheckStatus.Missing, "文件缺失", Path.Combine(installRoot, "config", "install-manifest.ini")),
|
|
new StartupCheckItem("file:resources.pri", "resources.pri", StartupCheckSeverity.Warning, StartupCheckStatus.Missing, "文件缺失", Path.Combine(installRoot, "resources.pri")),
|
|
new StartupCheckItem("lang:zh-CN", "语言资源 zh-CN", StartupCheckSeverity.Warning, StartupCheckStatus.Missing, "语言目录缺失", Path.Combine(installRoot, "lang", "zh-CN")),
|
|
new StartupCheckItem("lang:root:fr-FR", "根目录语言资源 fr-FR", StartupCheckSeverity.Warning, StartupCheckStatus.Failed, "旧版误判", Path.Combine(installRoot, "fr-FR"))
|
|
]);
|
|
await store.SaveAsync(oldReport);
|
|
|
|
var service = new InstallIntegrityCheckService(paths, store);
|
|
|
|
var latest = await service.GetLatestCurrentReportAsync(StartupCheckKind.FastPreflight);
|
|
var history = await service.GetCurrentHistoryAsync(10);
|
|
|
|
Assert.IsNotNull(latest);
|
|
Assert.AreEqual(0, latest.VisibleIssueCount);
|
|
Assert.AreEqual(StartupCheckStatus.Skipped, latest.Items.First(item => item.Id == "manifest").Status);
|
|
Assert.AreEqual(StartupCheckStatus.Passed, latest.Items.First(item => item.Id == "file:resources.pri").Status);
|
|
Assert.AreEqual(StartupCheckStatus.Passed, latest.Items.First(item => item.Id == "lang:zh-CN").Status);
|
|
Assert.AreEqual(StartupCheckStatus.Skipped, latest.Items.First(item => item.Id == "lang:root:fr-FR").Status);
|
|
Assert.AreEqual(0, history[0].VisibleIssueCount);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task MonthlyIntegrityUsesRecentReportUntilManifestIdentityChanges()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-monthly");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
var now = new DateTimeOffset(2026, 6, 14, 9, 0, 0, TimeSpan.Zero);
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var store = new StartupCheckStore(paths);
|
|
var service = new InstallIntegrityCheckService(paths, store, clock: () => now);
|
|
|
|
var first = await service.RunMonthlyIntegrityCheckAsync();
|
|
now = now.AddDays(5);
|
|
var cached = await service.RunMonthlyIntegrityCheckAsync();
|
|
|
|
Assert.AreEqual(first.Id, cached.Id);
|
|
|
|
File.AppendAllText(Path.Combine(installRoot, InstallManifest.RelativePath.Replace('/', Path.DirectorySeparatorChar)), Environment.NewLine + "; changed");
|
|
now = now.AddDays(1);
|
|
var changed = await service.RunMonthlyIntegrityCheckAsync();
|
|
|
|
Assert.AreNotEqual(first.Id, changed.Id);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task MonthlyIntegrityDoesNotReuseReportFromDifferentInstallRoot()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-monthly-root");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var secondRoot = workspace.CreateDirectory("install2");
|
|
CopyDirectory(installRoot, secondRoot);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
var now = new DateTimeOffset(2026, 6, 14, 9, 0, 0, TimeSpan.Zero);
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var store = new StartupCheckStore(paths);
|
|
var service = new InstallIntegrityCheckService(paths, store, clock: () => now);
|
|
var first = await service.RunMonthlyIntegrityCheckAsync();
|
|
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, secondRoot);
|
|
now = now.AddDays(1);
|
|
var second = await service.RunMonthlyIntegrityCheckAsync();
|
|
|
|
Assert.AreNotEqual(first.Id, second.Id);
|
|
Assert.AreNotEqual(first.InstallIdentity, second.InstallIdentity);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task IntegrityReportsMissingExternalToolLaunchableAsWarning()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-tool-missing");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
Directory.CreateDirectory(Path.Combine(installRoot, "Tools", "Sample"));
|
|
File.WriteAllText(Path.Combine(installRoot, "Tools", "Sample", "present.exe"), string.Empty);
|
|
File.AppendAllText(
|
|
Path.Combine(installRoot, InstallManifest.RelativePath.Replace('/', Path.DirectorySeparatorChar)),
|
|
Environment.NewLine + "Tools/Sample/missing.exe");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
|
|
|
|
var report = await service.RunManualIntegrityCheckAsync();
|
|
var missing = report.Items.FirstOrDefault(item => item.Id == "file:Tools/Sample/missing.exe");
|
|
|
|
Assert.IsNotNull(missing);
|
|
Assert.AreEqual(StartupCheckStatus.Missing, missing.Status);
|
|
Assert.AreEqual(StartupCheckSeverity.Warning, missing.Severity);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task UserDataRuntimePayloadIsRemovedBeforePreflight()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-userdata");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
var runtimeRoot = Path.Combine(appRoot, "Runtime");
|
|
var runtimesRoot = Path.Combine(appRoot, "runtimes");
|
|
Directory.CreateDirectory(runtimeRoot);
|
|
Directory.CreateDirectory(runtimesRoot);
|
|
File.WriteAllText(Path.Combine(runtimeRoot, "YMhutBox.exe"), string.Empty);
|
|
File.WriteAllText(Path.Combine(runtimesRoot, "Microsoft.ui.xaml.dll"), string.Empty);
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
Assert.IsFalse(Directory.Exists(runtimeRoot));
|
|
Assert.IsFalse(Directory.Exists(runtimesRoot));
|
|
|
|
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
|
|
|
|
var report = await service.RunFastPreflightAsync();
|
|
var runtime = report.Items.FirstOrDefault(item => item.Id == "userdata:runtime");
|
|
|
|
Assert.IsNotNull(runtime);
|
|
Assert.AreEqual(StartupCheckStatus.Passed, runtime.Status);
|
|
Assert.IsFalse(Directory.Exists(runtimeRoot));
|
|
Assert.IsFalse(Directory.Exists(runtimesRoot));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task UserDataToolsAndMetadataPayloadsAreRemovedBeforePreflight()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-userdata-tools");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var toolsRoot = Path.Combine(appRoot, "Tools");
|
|
var metadataRoot = Path.Combine(appRoot, "Metadata");
|
|
Directory.CreateDirectory(toolsRoot);
|
|
Directory.CreateDirectory(metadataRoot);
|
|
File.WriteAllText(Path.Combine(toolsRoot, "YMhutBox.exe"), string.Empty);
|
|
File.WriteAllText(Path.Combine(metadataRoot, "tools.json"), "{}");
|
|
|
|
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
|
|
|
|
var report = await service.RunFastPreflightAsync();
|
|
var tools = report.Items.FirstOrDefault(item => item.Id == "userdata:tools");
|
|
var metadata = report.Items.FirstOrDefault(item => item.Id == "userdata:metadata");
|
|
|
|
Assert.IsNotNull(tools);
|
|
Assert.IsNotNull(metadata);
|
|
Assert.AreEqual(StartupCheckStatus.Passed, tools.Status);
|
|
Assert.AreEqual(StartupCheckStatus.Passed, metadata.Status);
|
|
Assert.IsFalse(Directory.Exists(toolsRoot));
|
|
Assert.IsFalse(Directory.Exists(metadataRoot));
|
|
Assert.AreEqual(0, report.VisibleIssueCount);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task CurrentHistoryFiltersOutHistoricalInstallReports()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-current-history");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var secondRoot = workspace.CreateDirectory("install2");
|
|
CopyDirectory(installRoot, secondRoot);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var store = new StartupCheckStore(paths);
|
|
var service = new InstallIntegrityCheckService(paths, store);
|
|
var oldReport = await service.RunManualIntegrityCheckAsync();
|
|
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, secondRoot);
|
|
var currentReport = await service.RunManualIntegrityCheckAsync();
|
|
var all = await service.GetHistorySummariesAsync();
|
|
var current = await service.GetCurrentHistoryAsync();
|
|
|
|
Assert.IsTrue(all.Any(summary => summary.Id == oldReport.Id && !summary.IsCurrentInstall));
|
|
Assert.IsTrue(current.All(summary => summary.IsCurrentInstall));
|
|
Assert.IsTrue(current.Any(summary => summary.Id == currentReport.Id));
|
|
Assert.IsFalse(current.Any(summary => summary.Id == oldReport.Id));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task RecheckItemReportsCurrentPathPassesAfterFileRestored()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-recheck");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
var executable = Path.Combine(installRoot, "YMhutBox.exe");
|
|
File.Delete(executable);
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
|
|
var report = await service.RunManualIntegrityCheckAsync();
|
|
var missing = report.Items.First(item => item.Id == "file:YMhutBox.exe");
|
|
|
|
File.WriteAllText(executable, string.Empty);
|
|
var rechecked = await service.RecheckItemAsync(report.Id, missing.Id);
|
|
|
|
Assert.IsNotNull(rechecked);
|
|
Assert.AreEqual(StartupCheckStatus.Passed, rechecked.Status);
|
|
StringAssert.Contains(rechecked.Detail, "old record");
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task IntegrityDoesNotTreatNormalRootFoldersAsCultureFolders()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-root-folders");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
Directory.CreateDirectory(Path.Combine(installRoot, "Assets"));
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
|
|
|
|
var report = await service.RunManualIntegrityCheckAsync();
|
|
|
|
Assert.IsFalse(report.Items.Any(item => string.Equals(item.Id, "lang:root:Assets", StringComparison.OrdinalIgnoreCase)));
|
|
Assert.IsFalse(report.Items.Any(item => string.Equals(item.Id, "lang:root:Tools", StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task StartupCheckStorePersistsHistoryAndLatestReport()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-store");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var store = new StartupCheckStore(paths);
|
|
var service = new InstallIntegrityCheckService(paths, store);
|
|
|
|
var report = await service.RunManualIntegrityCheckAsync();
|
|
var latest = await store.GetLatestReportAsync();
|
|
var history = await store.GetHistoryAsync();
|
|
var summaries = await store.GetHistorySummariesAsync();
|
|
var loaded = await store.GetReportAsync(report.Id);
|
|
|
|
Assert.IsNotNull(latest);
|
|
Assert.AreEqual(report.Id, latest.Id);
|
|
Assert.IsGreaterThanOrEqualTo(history.Count, 1);
|
|
Assert.IsNotEmpty(history[0].Items);
|
|
Assert.IsGreaterThanOrEqualTo(summaries.Count, 1);
|
|
Assert.AreEqual(report.Id, summaries[0].Id);
|
|
Assert.AreEqual(report.Items.Count, summaries[0].ItemCount);
|
|
Assert.AreEqual(report.IssueCount, summaries[0].IssueCount);
|
|
Assert.IsNotNull(loaded);
|
|
Assert.AreEqual(report.Id, loaded.Id);
|
|
Assert.IsNotEmpty(loaded.Items);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task StartupCheckStoreMigratesLegacyDatabaseIntoMainSqlite()
|
|
{
|
|
using var workspace = TestWorkspace.Create("ymhut-startup-store-migrate");
|
|
var installRoot = CreateHealthyInstall(workspace);
|
|
var appRoot = workspace.CreateDirectory("user");
|
|
|
|
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
|
|
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
|
|
|
|
var paths = AppPaths.ForCurrentUser(appRoot);
|
|
var legacyPath = Path.Combine(paths.Data, "startup-checks.db");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(legacyPath)!);
|
|
await using (var connection = new SqliteConnection($"Data Source={legacyPath};Pooling=False"))
|
|
{
|
|
connection.Open();
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = """
|
|
CREATE TABLE startup_check_reports (
|
|
id TEXT PRIMARY KEY,
|
|
kind TEXT NOT NULL,
|
|
started_at TEXT NOT NULL,
|
|
completed_at TEXT NOT NULL,
|
|
install_root TEXT NOT NULL,
|
|
manifest_identity TEXT NOT NULL,
|
|
issue_count INTEGER NOT NULL,
|
|
critical_issue_count INTEGER NOT NULL,
|
|
warning_issue_count INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE startup_check_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
report_id TEXT NOT NULL,
|
|
item_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
severity TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
detail TEXT NOT NULL,
|
|
path TEXT NULL
|
|
);
|
|
INSERT INTO startup_check_reports(
|
|
id, kind, started_at, completed_at, install_root, manifest_identity,
|
|
issue_count, critical_issue_count, warning_issue_count)
|
|
VALUES(
|
|
'11111111111111111111111111111111',
|
|
'FastPreflight',
|
|
'2026-06-01T00:00:00.0000000+00:00',
|
|
'2026-06-01T00:00:01.0000000+00:00',
|
|
'C:\Legacy',
|
|
'legacy',
|
|
1,
|
|
0,
|
|
1);
|
|
INSERT INTO startup_check_items(report_id, item_id, name, severity, status, detail, path)
|
|
VALUES(
|
|
'11111111111111111111111111111111',
|
|
'legacy:item',
|
|
'旧记录',
|
|
'Warning',
|
|
'Failed',
|
|
'从旧库导入',
|
|
NULL);
|
|
""";
|
|
await command.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
var store = new StartupCheckStore(paths);
|
|
var latest = await store.GetLatestReportAsync();
|
|
|
|
Assert.IsNotNull(latest);
|
|
Assert.AreEqual("legacy", latest.ManifestIdentity);
|
|
Assert.HasCount(1, latest.Items);
|
|
Assert.IsFalse(File.Exists(legacyPath));
|
|
Assert.AreEqual(Path.Combine(paths.Logs, AppDatabasePaths.MainDatabaseFileName), store.DatabasePath);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task StartupInitializationPipelineReportsMonotonicWeightedProgress()
|
|
{
|
|
var progress = new List<StartupInitializationStageProgress>();
|
|
var pipeline = new StartupInitializationPipeline([
|
|
new StartupInitializationStage("first", "First", 1, true, (context, token) =>
|
|
{
|
|
context.Report(0.5, "Half");
|
|
return Task.CompletedTask;
|
|
}),
|
|
new StartupInitializationStage("second", "Second", 3, true, (context, token) =>
|
|
{
|
|
context.Report(0.25, "Quarter");
|
|
context.Report(0.75, "Almost");
|
|
return Task.CompletedTask;
|
|
})
|
|
]);
|
|
|
|
await pipeline.RunAsync(new InlineProgress<StartupInitializationStageProgress>(progress.Add));
|
|
|
|
Assert.IsNotEmpty(progress);
|
|
Assert.AreEqual(100, progress[^1].Progress);
|
|
for (var index = 1; index < progress.Count; index++)
|
|
{
|
|
Assert.IsGreaterThanOrEqualTo(
|
|
progress[index - 1].Progress,
|
|
progress[index].Progress,
|
|
$"Progress moved backwards at {index}: {progress[index - 1].Progress} -> {progress[index].Progress}");
|
|
}
|
|
|
|
CollectionAssert.AreEqual(new[] { "first", "second" }, pipeline.Stages.Select(stage => stage.Id).ToArray());
|
|
}
|
|
|
|
[TestMethod]
|
|
public void StartupSplashMessageSerializesStableContract()
|
|
{
|
|
var message = new StartupSplashMessage(
|
|
"progress",
|
|
"正在检查",
|
|
42,
|
|
"fast-preflight",
|
|
"快速预检",
|
|
8,
|
|
10,
|
|
"检查关键文件",
|
|
"warning",
|
|
IsRepairStep: true,
|
|
Theme: "dark",
|
|
ReducedMotion: true);
|
|
|
|
var json = message.ToJson();
|
|
|
|
StringAssert.Contains(json, "\"type\": \"progress\"");
|
|
StringAssert.Contains(json, "\"stageId\": \"fast-preflight\"");
|
|
StringAssert.Contains(json, "\"isRepairStep\": true");
|
|
StringAssert.Contains(json, "\"reducedMotion\": true");
|
|
}
|
|
|
|
private static string CreateHealthyInstall(TestWorkspace workspace)
|
|
{
|
|
var installRoot = workspace.CreateDirectory("install");
|
|
workspace.CreateFile(["install", "YMhutBox.exe"], string.Empty);
|
|
workspace.CreateFile(["install", "YMhutBox.dll"], string.Empty);
|
|
workspace.CreateFile(["install", "resources.pri"], string.Empty);
|
|
workspace.CreateFile(["install", "lang", "zh-CN", "Microsoft.ui.xaml.dll.mui"], string.Empty);
|
|
workspace.CreateFile(["install", "lang", "en-US", "Microsoft.ui.xaml.dll.mui"], string.Empty);
|
|
workspace.CreateFile(["install", "Tools", "Sample", "tool.exe"], string.Empty);
|
|
workspace.CreateFile(["install", "Metadata", "tools.json"], """{ "tools": [] }""");
|
|
workspace.CreateFile(["install", "config", "install-manifest.ini"],
|
|
"""
|
|
[Release]
|
|
Version=2.0.6
|
|
Build=1
|
|
Channel=stable
|
|
PackageVersion=2.0.6.1
|
|
|
|
[RequiredFiles]
|
|
YMhutBox.exe
|
|
YMhutBox.dll
|
|
resources.pri
|
|
|
|
[Files]
|
|
YMhutBox.exe
|
|
YMhutBox.dll
|
|
resources.pri
|
|
lang/zh-CN/Microsoft.ui.xaml.dll.mui
|
|
lang/en-US/Microsoft.ui.xaml.dll.mui
|
|
Tools/Sample/tool.exe
|
|
Metadata/tools.json
|
|
""");
|
|
return installRoot;
|
|
}
|
|
|
|
private static string CreateDevelopmentInstall(TestWorkspace workspace)
|
|
{
|
|
var installRoot = workspace.CreateDirectory("install");
|
|
workspace.CreateFile(["install", "YMhutBox.exe"], string.Empty);
|
|
workspace.CreateFile(["install", "YMhutBox.dll"], string.Empty);
|
|
workspace.CreateFile(["install", "zh-CN", "Microsoft.ui.xaml.dll.mui"], string.Empty);
|
|
workspace.CreateFile(["install", "en-US", "Microsoft.ui.xaml.dll.mui"], string.Empty);
|
|
return installRoot;
|
|
}
|
|
|
|
private static void CopyDirectory(string source, string destination)
|
|
{
|
|
Directory.CreateDirectory(destination);
|
|
foreach (var directory in Directory.EnumerateDirectories(source, "*", SearchOption.AllDirectories))
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(destination, Path.GetRelativePath(source, directory)));
|
|
}
|
|
|
|
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
|
|
{
|
|
var target = Path.Combine(destination, Path.GetRelativePath(source, file));
|
|
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
|
File.Copy(file, target, overwrite: true);
|
|
}
|
|
}
|
|
|
|
private sealed class EnvironmentScope : IDisposable
|
|
{
|
|
private readonly Dictionary<string, string?> _snapshot;
|
|
|
|
private EnvironmentScope(IEnumerable<string> names)
|
|
{
|
|
_snapshot = names.ToDictionary(name => name, Environment.GetEnvironmentVariable, StringComparer.Ordinal);
|
|
}
|
|
|
|
public static EnvironmentScope Capture(params string[] names) => new(names);
|
|
|
|
public void Set(string name, string? value) => Environment.SetEnvironmentVariable(name, value);
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var item in _snapshot)
|
|
{
|
|
Environment.SetEnvironmentVariable(item.Key, item.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class InlineProgress<T>(Action<T> report) : IProgress<T>
|
|
{
|
|
public void Report(T value) => report(value);
|
|
}
|
|
|
|
private sealed class TestWorkspace : IDisposable
|
|
{
|
|
private TestWorkspace(string root)
|
|
{
|
|
Root = root;
|
|
}
|
|
|
|
public string Root { get; }
|
|
|
|
public static TestWorkspace Create(string prefix)
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), prefix, Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
return new TestWorkspace(root);
|
|
}
|
|
|
|
public string CreateDirectory(params string[] parts)
|
|
{
|
|
var directory = Path.Combine([Root, .. parts]);
|
|
Directory.CreateDirectory(directory);
|
|
return directory;
|
|
}
|
|
|
|
public string CreateFile(string[] parts, string contents)
|
|
{
|
|
var path = Path.Combine([Root, .. parts]);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
File.WriteAllText(path, contents);
|
|
return path;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(Root))
|
|
{
|
|
Directory.Delete(Root, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
}
|