feat(launcher): stabilize prepared content updates
This commit is contained in:
parent
f160f3fee1
commit
af9327a17b
42 changed files with 3706 additions and 147 deletions
|
|
@ -43,4 +43,27 @@ public sealed class BakeProcessRunnerTests
|
|||
Assert.False(startInfo.Environment.ContainsKey(
|
||||
BakePublicationGuardPaths.NonceEnvironmentVariable));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilteredOverlayArgumentsUseBakeToolsPinnedHexLists()
|
||||
{
|
||||
var request = new BakeProcessRequest(
|
||||
"acdream-bake",
|
||||
"retail-dats",
|
||||
"data/pak/update.pak",
|
||||
4,
|
||||
DatIds: [0x01000001u, 0x02000002u],
|
||||
Landblocks: [0x0A, 0xFE]);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"--dat-dir", "retail-dats",
|
||||
"--out", "data/pak/update.pak",
|
||||
"--threads", "4",
|
||||
"--progress-json",
|
||||
"--ids", "0x01000001,0x02000002",
|
||||
"--landblocks", "0x0A,0xFE",
|
||||
],
|
||||
request.Arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
using System.Buffers.Binary;
|
||||
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 LauncherContentStateStoreTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"acdream-content-state-tests",
|
||||
Guid.NewGuid().ToString("N"));
|
||||
private readonly ApplicationPathSet _paths;
|
||||
|
||||
public LauncherContentStateStoreTests()
|
||||
{
|
||||
_paths = new ApplicationPathSet(
|
||||
Path.Combine(_root, "config"),
|
||||
Path.Combine(_root, "data"),
|
||||
Path.Combine(_root, "cache"),
|
||||
null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_root))
|
||||
{
|
||||
Directory.Delete(_root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchingSidecarResolvesWithoutStartupHashAndForceVerifyHashesBoth()
|
||||
{
|
||||
int hashCalls = 0;
|
||||
var store = new LauncherContentStateStore(
|
||||
_paths,
|
||||
async (path, cancellationToken) =>
|
||||
{
|
||||
hashCalls++;
|
||||
return await FileIntegrity.ComputeSha256HexAsync(
|
||||
path,
|
||||
cancellationToken);
|
||||
});
|
||||
string basePath = Path.Combine(_paths.DataDirectory, "pak", "acdream.pak");
|
||||
string overlayPath = Path.Combine(
|
||||
_paths.DataDirectory,
|
||||
"pak",
|
||||
"acdream-update-5-test.pak");
|
||||
WritePakHeader(basePath, recipe: 4);
|
||||
WritePakHeader(overlayPath, recipe: 5);
|
||||
LauncherInstallRecord record = await RecordAsync(basePath, recipe: 4);
|
||||
var overlay = new LauncherContentOverlay(
|
||||
Path.GetFileName(overlayPath),
|
||||
await FileIntegrity.ComputeSha256HexAsync(overlayPath),
|
||||
new FileInfo(overlayPath).Length,
|
||||
5);
|
||||
var state = new LauncherContentState(
|
||||
LauncherContentState.CurrentSchemaVersion,
|
||||
record.PreparedAssetSha256,
|
||||
5,
|
||||
overlay);
|
||||
await store.SaveAtomicallyAsync(record, state);
|
||||
|
||||
(LauncherContentState? quick, string? quickError) =
|
||||
await store.LoadAsync(record);
|
||||
|
||||
Assert.Null(quickError);
|
||||
Assert.Equal(state, quick);
|
||||
Assert.Equal(0, hashCalls);
|
||||
|
||||
(LauncherContentState? verified, string? verifyError) =
|
||||
await store.LoadAsync(record, forceFullVerification: true);
|
||||
Assert.Null(verifyError);
|
||||
Assert.Equal(state, verified);
|
||||
Assert.Equal(2, hashCalls);
|
||||
Assert.Empty(Directory.EnumerateFiles(
|
||||
Path.GetDirectoryName(store.StatePath)!,
|
||||
"content.current.json.*.tmp"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("../escape.pak")]
|
||||
[InlineData("nested/escape.pak")]
|
||||
[InlineData("C:\\escape.pak")]
|
||||
[InlineData("not-a-pak.txt")]
|
||||
public async Task OverlayPathMustBeOneContainedPakFilename(string path)
|
||||
{
|
||||
var store = new LauncherContentStateStore(_paths);
|
||||
string basePath = Path.Combine(_paths.DataDirectory, "pak", "acdream.pak");
|
||||
WritePakHeader(basePath, recipe: 4);
|
||||
LauncherInstallRecord record = await RecordAsync(basePath, recipe: 4);
|
||||
var state = new LauncherContentState(
|
||||
LauncherContentState.CurrentSchemaVersion,
|
||||
record.PreparedAssetSha256,
|
||||
5,
|
||||
new LauncherContentOverlay(path, new string('a', 64), 64, 5));
|
||||
|
||||
InvalidDataException error = await Assert.ThrowsAsync<InvalidDataException>(
|
||||
() => store.SaveAtomicallyAsync(record, state));
|
||||
|
||||
Assert.Contains("filename", error.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.False(File.Exists(store.StatePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BaseDigestBindingRejectsSidecarFromPriorFullRebuild()
|
||||
{
|
||||
var store = new LauncherContentStateStore(_paths);
|
||||
string basePath = Path.Combine(_paths.DataDirectory, "pak", "acdream.pak");
|
||||
WritePakHeader(basePath, recipe: 4);
|
||||
LauncherInstallRecord record = await RecordAsync(basePath, recipe: 4);
|
||||
var state = new LauncherContentState(
|
||||
LauncherContentState.CurrentSchemaVersion,
|
||||
new string('b', 64),
|
||||
5,
|
||||
new LauncherContentOverlay(
|
||||
"acdream-update-5-test.pak",
|
||||
new string('a', 64),
|
||||
64,
|
||||
5));
|
||||
|
||||
InvalidDataException error = await Assert.ThrowsAsync<InvalidDataException>(
|
||||
() => store.SaveAtomicallyAsync(record, state));
|
||||
|
||||
Assert.Contains("base pak", error.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static void WritePakHeader(string path, uint recipe)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
byte[] bytes = new byte[64];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(0, 4), 0x4B504341u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(4, 4), 1);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(8, 4), 100);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(12, 4), 200);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(16, 4), 300);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(20, 4), 400);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(24, 8), 64);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(36, 4), recipe);
|
||||
File.WriteAllBytes(path, bytes);
|
||||
}
|
||||
|
||||
internal static async Task<LauncherInstallRecord> RecordAsync(
|
||||
string basePath,
|
||||
uint recipe,
|
||||
string? datDirectory = null) =>
|
||||
new(
|
||||
datDirectory ?? Path.GetDirectoryName(basePath)!,
|
||||
basePath,
|
||||
await FileIntegrity.ComputeSha256HexAsync(basePath),
|
||||
new FileInfo(basePath).Length,
|
||||
recipe);
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ public sealed class LauncherInstallRecordStoreTests : IDisposable
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StaleBakeToolVersionIsRejectedBeforeHashing()
|
||||
public async Task StaleBakeToolVersionPromptsForKnownMigrationBeforeHashing()
|
||||
{
|
||||
int hashCalls = 0;
|
||||
var store = new LauncherInstallRecordStore(
|
||||
|
|
@ -112,8 +112,12 @@ public sealed class LauncherInstallRecordStoreTests : IDisposable
|
|||
}));
|
||||
|
||||
InstallRecordVerification verification = await store.LoadAndVerifyAsync();
|
||||
Assert.Equal(InstallRecordVerificationState.Invalid, verification.State);
|
||||
Assert.Contains("Bake tool version", verification.Status, StringComparison.Ordinal);
|
||||
Assert.Equal(
|
||||
InstallRecordVerificationState.ContentUpdateRequired,
|
||||
verification.State);
|
||||
Assert.NotNull(verification.Record);
|
||||
Assert.Equal(ContentWorkKind.FullRebuild, verification.RequiredContentWork?.Kind);
|
||||
Assert.Contains("World data update", verification.Status, StringComparison.Ordinal);
|
||||
Assert.Equal(0, hashCalls);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,12 +75,14 @@ public sealed class LauncherInstallerTests : IDisposable
|
|||
Assert.Equal(Path.GetFullPath(_bakeExecutable), observedRequest.ExecutablePath);
|
||||
Assert.Equal(Path.GetFullPath(_dats), observedRequest.DatDirectory);
|
||||
Assert.Equal(
|
||||
Path.Combine(_paths.DataDirectory, "pak", "acdream.pak"),
|
||||
LauncherInstaller.GetFullRebuildCandidatePath(
|
||||
Path.Combine(_paths.DataDirectory, "pak", "acdream.pak")),
|
||||
observedRequest.OutputPath);
|
||||
Assert.Equal(
|
||||
[
|
||||
"--dat-dir", Path.GetFullPath(_dats),
|
||||
"--out", Path.Combine(_paths.DataDirectory, "pak", "acdream.pak"),
|
||||
"--out", LauncherInstaller.GetFullRebuildCandidatePath(
|
||||
Path.Combine(_paths.DataDirectory, "pak", "acdream.pak")),
|
||||
"--threads", "7",
|
||||
"--progress-json",
|
||||
],
|
||||
|
|
@ -145,6 +147,54 @@ public sealed class LauncherInstallerTests : IDisposable
|
|||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullContentMigrationPersistsClientGateAcrossLauncherRestart()
|
||||
{
|
||||
var runner = new FakeBakeProcessRunner(async (request, output, _) =>
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(request.OutputPath)!);
|
||||
await File.WriteAllTextAsync(request.OutputPath, "replacement package");
|
||||
long bytes = new FileInfo(request.OutputPath).Length;
|
||||
output($"{{\"v\":1,\"e\":\"started\",\"bakeToolVersion\":"
|
||||
+ $"{LauncherInstallRecordStore.CurrentBakeToolVersion}}}\n");
|
||||
output($"{{\"v\":1,\"e\":\"completed\",\"bakeToolVersion\":"
|
||||
+ $"{LauncherInstallRecordStore.CurrentBakeToolVersion},"
|
||||
+ $"\"outputBytes\":{bytes},\"failures\":0}}\n");
|
||||
return new BakeProcessResult(0, string.Empty);
|
||||
});
|
||||
var installer = new LauncherInstaller(
|
||||
_paths,
|
||||
_bakeExecutable,
|
||||
processRunner: runner);
|
||||
var migration = new ContentMigrationPlan(
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion - 1,
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion,
|
||||
ContentWorkKind.FullRebuild,
|
||||
"fixture full migration");
|
||||
|
||||
LauncherInstallResult result = await installer.ApplyContentUpdateAsync(
|
||||
_dats,
|
||||
2,
|
||||
migration);
|
||||
|
||||
var stateStore = new LauncherContentStateStore(_paths);
|
||||
Assert.True(result.Record.RequiresClientCompatibilityConfirmation);
|
||||
Assert.True(File.Exists(stateStore.ClientCompatibilityPendingPath));
|
||||
|
||||
var restarted = new LauncherInstaller(
|
||||
_paths,
|
||||
_bakeExecutable,
|
||||
processRunner: runner);
|
||||
InstallRecordVerification discovered = await restarted.LoadExistingAsync();
|
||||
Assert.True(discovered.IsVerified);
|
||||
Assert.True(discovered.Record!.RequiresClientCompatibilityConfirmation);
|
||||
|
||||
restarted.ConfirmClientCompatibility();
|
||||
discovered = await restarted.LoadExistingAsync();
|
||||
Assert.False(discovered.Record!.RequiresClientCompatibilityConfirmation);
|
||||
Assert.False(File.Exists(stateStore.ClientCompatibilityPendingPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FailedChildRestoresPriorVerifiedPakAndRecord()
|
||||
{
|
||||
|
|
@ -361,12 +411,15 @@ public sealed class LauncherInstallerTests : IDisposable
|
|||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => operationB);
|
||||
Assert.False(runnerBEntered);
|
||||
Assert.Equal(
|
||||
"installer A in progress",
|
||||
await File.ReadAllTextAsync(storeA.PreparedAssetPath));
|
||||
Assert.Equal(
|
||||
"previous verified package",
|
||||
await File.ReadAllTextAsync(backupPath));
|
||||
await File.ReadAllTextAsync(storeA.PreparedAssetPath));
|
||||
Assert.False(File.Exists(backupPath));
|
||||
Assert.Equal(
|
||||
"installer A in progress",
|
||||
await File.ReadAllTextAsync(
|
||||
LauncherInstaller.GetFullRebuildCandidatePath(
|
||||
storeA.PreparedAssetPath)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -547,12 +600,14 @@ public sealed class LauncherInstallerTests : IDisposable
|
|||
await File.ReadAllTextAsync(childPid),
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
using Process orphan = Process.GetProcessById(orphanPid);
|
||||
Assert.True(File.Exists(
|
||||
Assert.False(File.Exists(
|
||||
LauncherInstallRecordStore.GetBackupPath(
|
||||
store.PreparedAssetPath)));
|
||||
string candidatePath = LauncherInstaller.GetFullRebuildCandidatePath(
|
||||
store.PreparedAssetPath);
|
||||
Assert.True(File.Exists(
|
||||
BakePublicationGuardPaths.GetAuthorizationPath(
|
||||
store.PreparedAssetPath)));
|
||||
candidatePath)));
|
||||
|
||||
parent.Kill(entireProcessTree: false);
|
||||
await parent.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
|
@ -580,7 +635,7 @@ public sealed class LauncherInstallerTests : IDisposable
|
|||
recovered = await recovery.WaitAsync(TimeSpan.FromSeconds(15));
|
||||
Assert.False(File.Exists(
|
||||
BakePublicationGuardPaths.GetAuthorizationPath(
|
||||
store.PreparedAssetPath)));
|
||||
candidatePath)));
|
||||
File.WriteAllText(release, "release");
|
||||
}
|
||||
|
||||
|
|
@ -622,7 +677,7 @@ public sealed class LauncherInstallerTests : IDisposable
|
|||
Assert.False(backupAfterRecovery);
|
||||
Assert.False(File.Exists(
|
||||
BakePublicationGuardPaths.GetAuthorizationPath(
|
||||
store.PreparedAssetPath)));
|
||||
candidatePath)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,21 @@ public sealed class PreparedAssetVerificationCacheTests : IDisposable
|
|||
Assert.Equal(1, _hashCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CacheMissReportsTheExceptionalLongReadBeforeHashing()
|
||||
{
|
||||
LauncherInstallRecordStore store = await CreateInstalledStoreAsync();
|
||||
var statuses = new List<string>();
|
||||
|
||||
Assert.True((await store.LoadAndVerifyAsync(
|
||||
progress: new ImmediateProgress(statuses.Add))).IsVerified);
|
||||
|
||||
Assert.Contains(
|
||||
statuses,
|
||||
status => status.Contains("whole world-data pak", StringComparison.Ordinal)
|
||||
&& status.Contains("30 seconds", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForcedFullVerificationHashesEvenWithAValidCache()
|
||||
{
|
||||
|
|
@ -264,4 +279,9 @@ public sealed class PreparedAssetVerificationCacheTests : IDisposable
|
|||
LauncherInstallRecordStore.CurrentBakeToolVersion));
|
||||
return store;
|
||||
}
|
||||
|
||||
private sealed class ImmediateProgress(Action<string> report) : IProgress<string>
|
||||
{
|
||||
public void Report(string value) => report(value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue