acdream/tests/AcDream.Launcher.Core.Tests/Updates/UpdateSessionBarrierTests.cs

147 lines
5 KiB
C#

using System.Diagnostics;
using AcDream.Launcher.Core.Updates;
namespace AcDream.Launcher.Core.Tests.Updates;
public sealed class UpdateSessionBarrierTests : IDisposable
{
private readonly string _root = Path.Combine(
Path.GetTempPath(),
"acdream-update-lease-tests",
Guid.NewGuid().ToString("N"));
public void Dispose()
{
if (Directory.Exists(_root))
{
Directory.Delete(_root, recursive: true);
}
}
[Fact]
public void SharedSessionsCoexistAndExcludeUpdateTransactions()
{
var barrier = new UpdateSessionBarrier(_root);
using UpdateSessionBarrier.SessionLease first = barrier.AcquireSession();
using UpdateSessionBarrier.SessionLease second = barrier.AcquireSession();
LauncherUpdateException blocked = Assert.Throws<LauncherUpdateException>(
barrier.AcquireExclusive);
Assert.Contains("session", blocked.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void ExclusiveUpdaterExcludesSessionsAndConcurrentUpdater()
{
var barrier = new UpdateSessionBarrier(_root);
using UpdateSessionBarrier.ExclusiveLease update = barrier.AcquireExclusive();
Assert.Throws<LauncherUpdateException>(barrier.AcquireSession);
Assert.Throws<LauncherUpdateException>(barrier.AcquireExclusive);
}
[Theory]
[InlineData("session")]
[InlineData("exclusive")]
public async Task CrossProcessLeaseRefusesRacingLauncherAndReleasesCleanly(string mode)
{
string ready = Path.Combine(_root, mode + ".ready");
string release = Path.Combine(_root, mode + ".release");
Directory.CreateDirectory(_root);
string fixture = GetFixturePath();
Assert.True(File.Exists(fixture), $"Missing fixture: {fixture}");
var startInfo = new ProcessStartInfo("dotnet")
{
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
};
startInfo.ArgumentList.Add(fixture);
startInfo.ArgumentList.Add("hold-update-lease");
startInfo.ArgumentList.Add(mode);
startInfo.ArgumentList.Add(_root);
startInfo.ArgumentList.Add(ready);
startInfo.ArgumentList.Add(release);
using Process holder = Process.Start(startInfo)
?? throw new InvalidOperationException("Could not start update lease fixture.");
try
{
await WaitForFileAsync(ready, holder);
var barrier = new UpdateSessionBarrier(_root);
Assert.Throws<LauncherUpdateException>(barrier.AcquireExclusive);
if (mode == "session")
{
using UpdateSessionBarrier.SessionLease peer = barrier.AcquireSession();
}
else
{
Assert.Throws<LauncherUpdateException>(barrier.AcquireSession);
}
await File.WriteAllTextAsync(release, "release");
await holder.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
Assert.Equal(0, holder.ExitCode);
using UpdateSessionBarrier.ExclusiveLease after = barrier.AcquireExclusive();
}
finally
{
if (!holder.HasExited)
{
holder.Kill(entireProcessTree: true);
await holder.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
}
}
}
private static async Task WaitForFileAsync(string path, Process process)
{
DateTimeOffset deadline = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(10);
while (!File.Exists(path))
{
if (process.HasExited)
{
throw new InvalidOperationException(
$"Lease fixture exited early with {process.ExitCode}: "
+ await process.StandardError.ReadToEndAsync());
}
if (DateTimeOffset.UtcNow >= deadline)
{
throw new TimeoutException("Lease fixture did not become ready.");
}
await Task.Delay(20);
}
}
private static string GetFixturePath()
{
string root = FindRepositoryRoot();
string configuration = new DirectoryInfo(AppContext.BaseDirectory)
.Parent?.Name ?? "Release";
return Path.Combine(
root,
"tests",
"AcDream.Launcher.Core.Tests.Fixtures.InstallLeaseHolder",
"bin",
configuration,
"net10.0",
"AcDream.Launcher.Core.Tests.Fixtures.InstallLeaseHolder.dll");
}
private static string FindRepositoryRoot()
{
for (var directory = new DirectoryInfo(AppContext.BaseDirectory);
directory is not null;
directory = directory.Parent)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
{
return directory.FullName;
}
}
throw new InvalidOperationException("Repository root was not found.");
}
}