mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-08-19 10:33:24 +08:00
feat: implement AirAppRuntimeBridge for managing runtime lifecycle and add corresponding unit tests for launcher window and runtime orchestration
This commit is contained in:
194
LanMountainDesktop.Tests/AirAppRuntimeBridgeTests.cs
Normal file
194
LanMountainDesktop.Tests/AirAppRuntimeBridgeTests.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using LanMountainDesktop.Launcher.Shell;
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class AirAppRuntimeBridgeTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EnsureStartedAsync_StartsRuntimeAndReturnsConfirmedAvailability()
|
||||
{
|
||||
var statusCalls = 0;
|
||||
var backend = new TestBackend
|
||||
{
|
||||
GetStatusHandler = () => ++statusCalls == 1
|
||||
? Task.FromException<AirAppRuntimeStatus>(new IOException("pipe is not ready"))
|
||||
: Task.FromResult(CreateStatus(0, hostAlive: false))
|
||||
};
|
||||
var bridge = new AirAppRuntimeBridge("C:\\app", "C:\\data", backend);
|
||||
|
||||
var result = await bridge.EnsureStartedAsync();
|
||||
|
||||
Assert.True(result.Available);
|
||||
Assert.Equal("started", result.Code);
|
||||
Assert.NotNull(result.Status);
|
||||
Assert.Single(backend.StartRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureStartedAsync_WhenProcessIsNotCreated_ReturnsWithoutPolling()
|
||||
{
|
||||
var backend = new TestBackend
|
||||
{
|
||||
StartHandler = _ => null
|
||||
};
|
||||
var bridge = new AirAppRuntimeBridge("C:\\app", null, backend);
|
||||
|
||||
var result = await bridge.EnsureStartedAsync();
|
||||
|
||||
Assert.False(result.Available);
|
||||
Assert.Equal("process_not_created", result.Code);
|
||||
Assert.Equal(1, backend.GetStatusCalls);
|
||||
Assert.Single(backend.StartRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttachHostAsync_RestartsRuntimeWhenItDisappearsBeforeAttach()
|
||||
{
|
||||
var hostProcessId = Environment.ProcessId;
|
||||
var statusCalls = 0;
|
||||
var attachCalls = 0;
|
||||
var backend = new TestBackend
|
||||
{
|
||||
GetStatusHandler = () => ++statusCalls switch
|
||||
{
|
||||
1 => Task.FromResult(CreateStatus(0, hostAlive: false)),
|
||||
2 => Task.FromException<AirAppRuntimeStatus>(new IOException("runtime exited")),
|
||||
_ => Task.FromResult(CreateStatus(0, hostAlive: false))
|
||||
},
|
||||
AttachHandler = processId => ++attachCalls == 1
|
||||
? Task.FromException<AirAppRuntimeControlResult>(new IOException("connection closed"))
|
||||
: Task.FromResult(CreateAttachResult(processId, accepted: true, hostAlive: true))
|
||||
};
|
||||
var bridge = new AirAppRuntimeBridge("C:\\app", null, backend);
|
||||
|
||||
var result = await bridge.AttachHostAsync(hostProcessId);
|
||||
|
||||
Assert.True(result.Accepted);
|
||||
Assert.Equal("host_attached", result.Code);
|
||||
Assert.Equal(2, result.Attempts);
|
||||
Assert.Equal(2, attachCalls);
|
||||
Assert.Single(backend.StartRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttachHostAsync_RetriesAnUnconfirmedAttachResult()
|
||||
{
|
||||
var hostProcessId = Environment.ProcessId;
|
||||
var attachCalls = 0;
|
||||
var backend = new TestBackend
|
||||
{
|
||||
GetStatusHandler = () => Task.FromResult(CreateStatus(0, hostAlive: false)),
|
||||
AttachHandler = processId => Task.FromResult(++attachCalls == 1
|
||||
? CreateAttachResult(processId + 1, accepted: true, hostAlive: true)
|
||||
: CreateAttachResult(processId, accepted: true, hostAlive: true))
|
||||
};
|
||||
var bridge = new AirAppRuntimeBridge("C:\\app", null, backend);
|
||||
|
||||
var result = await bridge.AttachHostAsync(hostProcessId);
|
||||
|
||||
Assert.True(result.Accepted);
|
||||
Assert.Equal(2, result.Attempts);
|
||||
Assert.Equal(hostProcessId, result.Status!.HostProcessId);
|
||||
Assert.Empty(backend.StartRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttachHostAsync_InvalidHostPidReturnsFailureWithoutRuntimeWork()
|
||||
{
|
||||
var backend = new TestBackend();
|
||||
var bridge = new AirAppRuntimeBridge("C:\\app", null, backend);
|
||||
|
||||
var result = await bridge.AttachHostAsync(0);
|
||||
|
||||
Assert.False(result.Accepted);
|
||||
Assert.Equal("invalid_host_pid", result.Code);
|
||||
Assert.Equal(0, result.Attempts);
|
||||
Assert.Equal(0, backend.GetStatusCalls);
|
||||
Assert.Empty(backend.StartRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttachHostAsync_ReturnsObservableFailureAfterBoundedRetries()
|
||||
{
|
||||
var attachCalls = 0;
|
||||
var backend = new TestBackend
|
||||
{
|
||||
GetStatusHandler = () => Task.FromResult(CreateStatus(0, hostAlive: false)),
|
||||
AttachHandler = processId =>
|
||||
{
|
||||
attachCalls++;
|
||||
return Task.FromResult(CreateAttachResult(processId, accepted: false, hostAlive: false));
|
||||
}
|
||||
};
|
||||
var bridge = new AirAppRuntimeBridge("C:\\app", null, backend);
|
||||
|
||||
var result = await bridge.AttachHostAsync(Environment.ProcessId);
|
||||
|
||||
Assert.False(result.Accepted);
|
||||
Assert.Equal("host_attach_unconfirmed", result.Code);
|
||||
Assert.Equal(4, result.Attempts);
|
||||
Assert.Equal(4, attachCalls);
|
||||
}
|
||||
|
||||
private static AirAppRuntimeControlResult CreateAttachResult(
|
||||
int hostProcessId,
|
||||
bool accepted,
|
||||
bool hostAlive)
|
||||
{
|
||||
return new AirAppRuntimeControlResult(
|
||||
accepted,
|
||||
accepted ? "host_attached" : "host_attach_rejected",
|
||||
"test result",
|
||||
CreateStatus(hostProcessId, hostAlive));
|
||||
}
|
||||
|
||||
private static AirAppRuntimeStatus CreateStatus(int hostProcessId, bool hostAlive)
|
||||
{
|
||||
return new AirAppRuntimeStatus(
|
||||
Environment.ProcessId,
|
||||
0,
|
||||
hostProcessId,
|
||||
false,
|
||||
hostAlive,
|
||||
false,
|
||||
DateTimeOffset.UtcNow,
|
||||
DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private sealed class TestBackend : IAirAppRuntimeBridgeBackend
|
||||
{
|
||||
public Func<Task<AirAppRuntimeStatus>> GetStatusHandler { get; init; } =
|
||||
() => Task.FromException<AirAppRuntimeStatus>(new IOException("runtime unavailable"));
|
||||
|
||||
public Func<int, Task<AirAppRuntimeControlResult>> AttachHandler { get; init; } =
|
||||
_ => Task.FromException<AirAppRuntimeControlResult>(new IOException("runtime unavailable"));
|
||||
|
||||
public Func<AirAppRuntimeStartRequest, int?> StartHandler { get; init; } = _ => 4242;
|
||||
|
||||
public List<AirAppRuntimeStartRequest> StartRequests { get; } = [];
|
||||
|
||||
public int GetStatusCalls { get; private set; }
|
||||
|
||||
public int? Start(AirAppRuntimeStartRequest request)
|
||||
{
|
||||
StartRequests.Add(request);
|
||||
return StartHandler(request);
|
||||
}
|
||||
|
||||
public Task<AirAppRuntimeStatus> GetStatusAsync()
|
||||
{
|
||||
GetStatusCalls++;
|
||||
return GetStatusHandler();
|
||||
}
|
||||
|
||||
public Task<AirAppRuntimeControlResult> AttachHostAsync(int hostProcessId)
|
||||
{
|
||||
return AttachHandler(hostProcessId);
|
||||
}
|
||||
|
||||
public Task DelayAsync(TimeSpan delay) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user