Files
cursor-lang/CursorLang.Core.Tests/Services/SingleInstanceGateTests.cs
T
alex 6259dbd6b3
Pull request / build (pull_request) Successful in 53s
lightweight variant
2026-08-12 16:01:54 +05:00

208 lines
5.8 KiB
C#

using System.Collections.Concurrent;
using CursorLang.Core.Services;
using CursorLang.Tests.Shared;
namespace CursorLang.Core.Tests.Services;
/// <summary>
/// The place of the single instance. The kernel object names in the tests are
/// their own: sharing them with a running application is not an option.
/// </summary>
public sealed class SingleInstanceGateTests
{
[Fact]
public void The_first_run_takes_the_place()
{
string suffix = UniqueSuffix();
SingleInstanceGate gate = Pump.Run(() => new SingleInstanceGate(suffix));
try
{
Assert.True(Pump.Run(gate.TryAcquire));
}
finally
{
Pump.Run(gate.Dispose);
}
}
[Fact]
public void The_second_run_does_not_get_the_place()
{
string suffix = UniqueSuffix();
SingleInstanceGate first = Pump.Run(() => new SingleInstanceGate(suffix));
try
{
Assert.True(Pump.Run(first.TryAcquire));
Assert.False(TryAcquireApart(suffix));
}
finally
{
Pump.Run(first.Dispose);
}
}
[Fact]
public void The_second_run_asks_the_running_one_to_show_its_window()
{
string suffix = UniqueSuffix();
SingleInstanceGate first = Pump.Run(() => new SingleInstanceGate(suffix));
ConcurrentQueue<EventArgs> requests = new();
first.ActivationRequested += (_, e) => requests.Enqueue(e);
try
{
Pump.Run(first.TryAcquire);
Assert.False(TryAcquireApart(suffix));
Pump.WaitFor(() => !requests.IsEmpty, "the running instance got the request to show itself");
}
finally
{
Pump.Run(first.Dispose);
}
}
[Fact]
public void Without_a_second_run_no_request_arrives()
{
string suffix = UniqueSuffix();
SingleInstanceGate gate = Pump.Run(() => new SingleInstanceGate(suffix));
ConcurrentQueue<EventArgs> requests = new();
gate.ActivationRequested += (_, e) => requests.Enqueue(e);
try
{
Pump.Run(gate.TryAcquire);
Pump.Pause(TimeSpan.FromMilliseconds(80));
Assert.Empty(requests);
}
finally
{
Pump.Run(gate.Dispose);
}
}
// The place is released on exit — otherwise the app would never start again
[Fact]
public void After_the_exit_the_place_is_free_again()
{
string suffix = UniqueSuffix();
SingleInstanceGate first = Pump.Run(() => new SingleInstanceGate(suffix));
Assert.True(Pump.Run(first.TryAcquire));
Pump.Run(first.Dispose);
Assert.True(TryAcquireApart(suffix));
}
[Fact]
public void No_requests_arrive_after_the_exit()
{
string suffix = UniqueSuffix();
SingleInstanceGate first = Pump.Run(() => new SingleInstanceGate(suffix));
ConcurrentQueue<EventArgs> requests = new();
first.ActivationRequested += (_, e) => requests.Enqueue(e);
Pump.Run(first.TryAcquire);
Pump.Run(first.Dispose);
// The place is free, so the new run simply takes it for itself
Assert.True(TryAcquireApart(suffix));
Pump.Pause(TimeSpan.FromMilliseconds(80));
Assert.Empty(requests);
}
// The previous instance crashed and did not release the place. It has no
// owner any more, which means the place is free
[Fact]
public void A_place_left_by_a_crash_counts_as_free()
{
string suffix = UniqueSuffix();
// A thread that took the mutex and ended without releasing it is exactly
// what a crashed application looks like to Windows
Pump.RunApart(() =>
{
var abandoned = new Mutex(initiallyOwned: false, "CursorLang.SingleInstance" + suffix);
abandoned.WaitOne(TimeSpan.Zero, exitContext: false);
});
SingleInstanceGate gate = Pump.Run(() => new SingleInstanceGate(suffix));
try
{
Assert.True(Pump.Run(gate.TryAcquire));
}
finally
{
Pump.Run(gate.Dispose);
}
}
[Fact]
public void Closing_without_taking_the_place_passes_without_consequence()
{
SingleInstanceGate gate = Pump.Run(() => new SingleInstanceGate(UniqueSuffix()));
Pump.Run(gate.Dispose);
}
// Each half of the application takes a place of its own: one background process
// and one settings window, and neither gets in the other's way
[Theory]
[InlineData(SingleInstanceGate.AgentName)]
[InlineData(SingleInstanceGate.SettingsName)]
public void Each_half_of_the_application_takes_a_place_of_its_own(string name)
{
SingleInstanceGate gate = Pump.Run(() => new SingleInstanceGate(name));
// The place may be held by a running application — then it is simply not taken
Pump.Run(gate.Dispose);
}
[Fact]
public void Closing_twice_passes_without_consequence()
{
SingleInstanceGate gate = Pump.Run(() => new SingleInstanceGate(UniqueSuffix()));
Pump.Run(gate.TryAcquire);
Pump.Run(gate.Dispose);
Pump.Run(gate.Dispose);
}
// Every test gets its own namespace of kernel objects
private static string UniqueSuffix() => "." + Guid.NewGuid().ToString("N");
/// <summary>
/// Tries to take the place the way a run started afterwards does it —
/// from another thread rather than from the same one.
/// </summary>
private static bool TryAcquireApart(string suffix)
{
bool acquired = false;
Pump.RunApart(() =>
{
var gate = new SingleInstanceGate(suffix);
try
{
acquired = gate.TryAcquire();
}
finally
{
gate.Dispose();
}
});
return acquired;
}
}