acdream/tests/AcDream.Launcher.Core.Tests/Installation/LauncherOverlayInstallerTests.cs
Erik af9327a17b
Some checks failed
CI / linux-portable (push) Failing after 3m12s
CI / windows-gate (push) Failing after 6m35s
CI / release (push) Has been skipped
feat(launcher): stabilize prepared content updates
2026-08-25 19:17:13 +02:00

218 lines
8.8 KiB
C#

using System.Text.Json;
using AcDream.Launcher.Core.Integrity;
using AcDream.Launcher.Core.Installation;
using AcDream.Launcher.Core.Launching;
using AcDream.Platform;
namespace AcDream.Launcher.Core.Tests.Installation;
public sealed class LauncherOverlayInstallerTests : IDisposable
{
private readonly string _root = Path.Combine(
Path.GetTempPath(),
"acdream-overlay-installer-tests",
Guid.NewGuid().ToString("N"));
private readonly ApplicationPathSet _paths;
private readonly string _dats;
private readonly string _bakeExecutable;
public LauncherOverlayInstallerTests()
{
_paths = new ApplicationPathSet(
Path.Combine(_root, "config"),
Path.Combine(_root, "data"),
Path.Combine(_root, "cache"),
null);
_dats = Path.Combine(_root, "dats");
foreach (string name in DatDirectoryLocator.RequiredFileNames)
{
Directory.CreateDirectory(_dats);
File.WriteAllText(Path.Combine(_dats, name), name);
}
_bakeExecutable = Path.Combine(_root, "bin", "acdream-bake");
Directory.CreateDirectory(Path.GetDirectoryName(_bakeExecutable)!);
File.WriteAllText(_bakeExecutable, "fixture");
}
public void Dispose()
{
if (Directory.Exists(_root))
{
Directory.Delete(_root, recursive: true);
}
}
[Fact]
public async Task FilteredBakePublishesOneOverlayWithoutRewritingBase()
{
var recordStore = new LauncherInstallRecordStore(_paths);
LauncherInstallRecord baseRecord = await CreateBaseRecordAsync(recordStore);
byte[] baseBefore = await File.ReadAllBytesAsync(
recordStore.PreparedAssetPath);
BakeProcessRequest? observed = null;
var runner = new FakeRunner(async (request, output, _) =>
{
observed = request;
LauncherContentStateStoreTests.WritePakHeader(
request.OutputPath,
LauncherInstallRecordStore.CurrentBakeToolVersion);
long bytes = new FileInfo(request.OutputPath).Length;
output($"{{\"v\":1,\"e\":\"started\",\"bakeToolVersion\":5}}\n");
output($"{{\"v\":1,\"e\":\"completed\",\"bakeToolVersion\":5,"
+ $"\"outputBytes\":{bytes},\"failures\":0}}\n");
await Task.Yield();
return new BakeProcessResult(0, string.Empty);
});
var installer = new LauncherInstaller(
_paths,
_bakeExecutable,
recordStore: recordStore,
processRunner: runner);
var migration = new ContentMigrationPlan(
4,
5,
ContentWorkKind.Overlay,
"bounded fixture update",
[0x01001234u, 0x02005678u],
[0xAB]);
LauncherInstallResult result = await installer.ApplyContentUpdateAsync(
_dats,
3,
migration);
Assert.Equal(baseBefore, await File.ReadAllBytesAsync(
recordStore.PreparedAssetPath));
Assert.NotNull(observed);
Assert.Equal(
new[] { "--ids", "0x01001234,0x02005678" },
observed.Arguments.SkipWhile(value => value != "--ids").Take(2));
Assert.Equal(
new[] { "--landblocks", "0xAB" },
observed.Arguments.SkipWhile(value => value != "--landblocks").Take(2));
Assert.NotNull(result.Record.PreparedAssetOverlayPath);
Assert.True(File.Exists(result.Record.PreparedAssetOverlayPath));
Assert.Equal(5u, result.Record.ResolvedBakeToolVersion);
Assert.True(result.Record.RequiresClientCompatibilityConfirmation);
Assert.False(File.Exists(
new LauncherContentStateStore(_paths).OverlayCandidatePath));
var restartedInstaller = new LauncherInstaller(
_paths,
_bakeExecutable,
recordStore: recordStore,
processRunner: runner);
InstallRecordVerification discovered =
await restartedInstaller.LoadExistingAsync();
Assert.True(discovered.IsVerified);
Assert.True(discovered.Record?.RequiresClientCompatibilityConfirmation);
Assert.Equal(
result.Record.PreparedAssetOverlayPath,
discovered.Record?.PreparedAssetOverlayPath);
restartedInstaller.ConfirmClientCompatibility();
discovered = await restartedInstaller.LoadExistingAsync();
Assert.True(discovered.IsVerified);
Assert.False(discovered.Record?.RequiresClientCompatibilityConfirmation);
string json = SessionConfigComposer.Serialize(
SessionConfigComposer.Compose(
new() { Name = "server", Host = "localhost", Port = 9000 },
new() { Account = "account", Password = "secret" },
new()
{
Name = "character",
LaunchMode = AcDream.Launcher.Core.Profiles.LaunchMode.Gui,
},
discovered.Record!,
_paths,
"overlay-session").Document);
Assert.Contains("preparedAssetOverlayPath", json, StringComparison.Ordinal);
Assert.Contains("preparedAssetBaseRecipeVersion", json, StringComparison.Ordinal);
Assert.DoesNotContain(baseRecord.PreparedAssetSha256, json, StringComparison.Ordinal);
}
[Fact]
public async Task CancellationLeavesBaseRecordAndSidecarUntouched()
{
var recordStore = new LauncherInstallRecordStore(_paths);
LauncherInstallRecord baseRecord = await CreateBaseRecordAsync(recordStore);
string recordBefore = await File.ReadAllTextAsync(recordStore.RecordPath);
byte[] baseBefore = await File.ReadAllBytesAsync(
recordStore.PreparedAssetPath);
var entered = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
var runner = new FakeRunner(async (request, _, cancellationToken) =>
{
LauncherContentStateStoreTests.WritePakHeader(request.OutputPath, 5);
entered.SetResult();
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
return new BakeProcessResult(0, string.Empty);
});
var installer = new LauncherInstaller(
_paths,
_bakeExecutable,
recordStore: recordStore,
processRunner: runner);
using var cancellation = new CancellationTokenSource();
Task<LauncherInstallResult> operation = installer.ApplyContentUpdateAsync(
_dats,
2,
new ContentMigrationPlan(
4,
5,
ContentWorkKind.Overlay,
"bounded fixture update",
[0x01001234u]),
cancellationToken: cancellation.Token);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
cancellation.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => operation);
Assert.Equal(baseBefore, await File.ReadAllBytesAsync(
recordStore.PreparedAssetPath));
Assert.Equal(recordBefore, await File.ReadAllTextAsync(recordStore.RecordPath));
var stateStore = new LauncherContentStateStore(_paths);
Assert.False(File.Exists(stateStore.StatePath));
Assert.False(File.Exists(stateStore.OverlayCandidatePath));
Assert.False(File.Exists(stateStore.ClientCompatibilityPendingPath));
Assert.Equal(
InstallRecordVerificationState.ContentUpdateRequired,
(await installer.LoadExistingAsync()).State);
Assert.Equal(baseRecord, (await recordStore.LoadAndVerifyAsync()).Record);
}
private async Task<LauncherInstallRecord> CreateBaseRecordAsync(
LauncherInstallRecordStore recordStore)
{
LauncherContentStateStoreTests.WritePakHeader(
recordStore.PreparedAssetPath,
recipe: 4);
LauncherInstallRecord baseRecord =
await LauncherContentStateStoreTests.RecordAsync(
recordStore.PreparedAssetPath,
recipe: 4,
datDirectory: Path.GetFullPath(_dats));
Directory.CreateDirectory(_paths.DataDirectory);
await File.WriteAllTextAsync(
recordStore.RecordPath,
JsonSerializer.Serialize(baseRecord, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}));
return baseRecord;
}
private sealed class FakeRunner(
Func<BakeProcessRequest, Action<string>, CancellationToken, Task<BakeProcessResult>> handler)
: IBakeProcessRunner
{
public Task<BakeProcessResult> RunAsync(
BakeProcessRequest request,
Action<string> onStandardOutput,
CancellationToken cancellationToken = default) =>
handler(request, onStandardOutput, cancellationToken);
}
}