205 lines
5.6 KiB
C#
205 lines
5.6 KiB
C#
using System.Collections.Concurrent;
|
|
using CursorLang.Services;
|
|
using CursorLang.Tests.Infrastructure;
|
|
|
|
namespace CursorLang.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 = Sta.Run(() => new SingleInstanceGate(suffix));
|
|
|
|
try
|
|
{
|
|
Assert.True(Sta.Run(gate.TryAcquire));
|
|
}
|
|
finally
|
|
{
|
|
Sta.Run(gate.Dispose);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void The_second_run_does_not_get_the_place()
|
|
{
|
|
string suffix = UniqueSuffix();
|
|
SingleInstanceGate first = Sta.Run(() => new SingleInstanceGate(suffix));
|
|
|
|
try
|
|
{
|
|
Assert.True(Sta.Run(first.TryAcquire));
|
|
Assert.False(TryAcquireApart(suffix));
|
|
}
|
|
finally
|
|
{
|
|
Sta.Run(first.Dispose);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void The_second_run_asks_the_running_one_to_show_its_window()
|
|
{
|
|
string suffix = UniqueSuffix();
|
|
SingleInstanceGate first = Sta.Run(() => new SingleInstanceGate(suffix));
|
|
|
|
ConcurrentQueue<EventArgs> requests = new();
|
|
first.ActivationRequested += (_, e) => requests.Enqueue(e);
|
|
|
|
try
|
|
{
|
|
Sta.Run(first.TryAcquire);
|
|
Assert.False(TryAcquireApart(suffix));
|
|
|
|
Sta.WaitFor(() => !requests.IsEmpty, "the running instance got the request to show itself");
|
|
}
|
|
finally
|
|
{
|
|
Sta.Run(first.Dispose);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Without_a_second_run_no_request_arrives()
|
|
{
|
|
string suffix = UniqueSuffix();
|
|
SingleInstanceGate gate = Sta.Run(() => new SingleInstanceGate(suffix));
|
|
|
|
ConcurrentQueue<EventArgs> requests = new();
|
|
gate.ActivationRequested += (_, e) => requests.Enqueue(e);
|
|
|
|
try
|
|
{
|
|
Sta.Run(gate.TryAcquire);
|
|
Sta.Pause(TimeSpan.FromMilliseconds(80));
|
|
|
|
Assert.Empty(requests);
|
|
}
|
|
finally
|
|
{
|
|
Sta.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 = Sta.Run(() => new SingleInstanceGate(suffix));
|
|
Assert.True(Sta.Run(first.TryAcquire));
|
|
Sta.Run(first.Dispose);
|
|
|
|
Assert.True(TryAcquireApart(suffix));
|
|
}
|
|
|
|
[Fact]
|
|
public void No_requests_arrive_after_the_exit()
|
|
{
|
|
string suffix = UniqueSuffix();
|
|
SingleInstanceGate first = Sta.Run(() => new SingleInstanceGate(suffix));
|
|
|
|
ConcurrentQueue<EventArgs> requests = new();
|
|
first.ActivationRequested += (_, e) => requests.Enqueue(e);
|
|
|
|
Sta.Run(first.TryAcquire);
|
|
Sta.Run(first.Dispose);
|
|
|
|
// The place is free, so the new run simply takes it for itself
|
|
Assert.True(TryAcquireApart(suffix));
|
|
Sta.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
|
|
Sta.RunApart(() =>
|
|
{
|
|
var abandoned = new Mutex(initiallyOwned: false, "CursorLang.SingleInstance" + suffix);
|
|
abandoned.WaitOne(TimeSpan.Zero, exitContext: false);
|
|
});
|
|
|
|
SingleInstanceGate gate = Sta.Run(() => new SingleInstanceGate(suffix));
|
|
|
|
try
|
|
{
|
|
Assert.True(Sta.Run(gate.TryAcquire));
|
|
}
|
|
finally
|
|
{
|
|
Sta.Run(gate.Dispose);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Closing_without_taking_the_place_passes_without_consequence()
|
|
{
|
|
SingleInstanceGate gate = Sta.Run(() => new SingleInstanceGate(UniqueSuffix()));
|
|
|
|
Sta.Run(gate.Dispose);
|
|
}
|
|
|
|
// The application takes the place under its ordinary name
|
|
[Fact]
|
|
public void The_ordinary_application_takes_the_place_under_its_own_name()
|
|
{
|
|
SingleInstanceGate gate = Sta.Run(() => new SingleInstanceGate());
|
|
|
|
// The place may be held by a running application — then it is simply not taken
|
|
Sta.Run(gate.Dispose);
|
|
}
|
|
|
|
[Fact]
|
|
public void Closing_twice_passes_without_consequence()
|
|
{
|
|
SingleInstanceGate gate = Sta.Run(() => new SingleInstanceGate(UniqueSuffix()));
|
|
|
|
Sta.Run(gate.TryAcquire);
|
|
Sta.Run(gate.Dispose);
|
|
Sta.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;
|
|
|
|
Sta.RunApart(() =>
|
|
{
|
|
var gate = new SingleInstanceGate(suffix);
|
|
|
|
try
|
|
{
|
|
acquired = gate.TryAcquire();
|
|
}
|
|
finally
|
|
{
|
|
gate.Dispose();
|
|
}
|
|
});
|
|
|
|
return acquired;
|
|
}
|
|
}
|