using System.Collections.Concurrent; using CursorLang.Core.Services; using CursorLang.Tests.Shared; namespace CursorLang.Core.Tests.Services; /// /// 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. /// 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 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 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 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"); /// /// Tries to take the place the way a run started afterwards does it — /// from another thread rather than from the same one. /// 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; } }