using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Text; namespace CursorLang.Tests; /// /// The application as a whole: the start, the single instance and the exit. /// /// /// The application cannot be built inside the tests — it raises windows and /// takes the place of the single instance for the whole session. It is /// therefore started the way the user starts it: as a separate process. /// /// If the application is already running in this session, the checks skip /// themselves: meddling with someone else's running instance is not their business. /// /// They skip themselves where there is no desktop to show a window on either — on a /// build agent living as a Windows service, for one. /// public sealed class EndToEndTests { private static readonly TimeSpan StartTimeout = TimeSpan.FromSeconds(30); private static readonly TimeSpan ExitTimeout = TimeSpan.FromSeconds(15); [Fact] public void The_application_starts_and_shows_the_settings_window() { using Launch launch = Launch.Start(); Assert.NotEqual(IntPtr.Zero, launch.WaitForWindow()); Assert.False(launch.Process.HasExited); } // A second run raises no second window but shows the window of the running one [Fact] public void The_second_run_ends_by_itself() { using Launch launch = Launch.Start(); launch.WaitForWindow(); using Process second = Launch.StartProcess(); Assert.True(second.WaitForExit(ExitTimeout), "the second run did not end by itself"); Assert.Equal(0, second.ExitCode); // And the first one keeps running Assert.False(launch.Process.HasExited); } [Fact] public void Closing_the_window_ends_the_application() { using Launch launch = Launch.Start(); launch.WaitForWindow(); Assert.True(launch.Process.CloseMainWindow(), "the window did not accept the request to close"); Assert.True(launch.Process.WaitForExit(ExitTimeout), "the application did not end after the window closed"); Assert.Equal(0, launch.Process.ExitCode); } /// A started application that shuts down together with the check. private sealed class Launch : IDisposable { private const int UOI_NAME = 2; private const string InteractiveWindowStation = "WinSta0"; [DllImport("user32.dll")] private static extern IntPtr GetProcessWindowStation(); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool GetUserObjectInformation(IntPtr hObj, int nIndex, StringBuilder pvInfo, int nLength, out int lpnLengthNeeded); private Launch(Process process) => Process = process; internal Process Process { get; } /// Starts the application first — making sure the place is free. internal static Launch Start() { if (!HasInteractiveDesktop()) { Assert.Skip("There is no interactive desktop here — the application has nowhere to show its window"); } if (Process.GetProcessesByName("CursorLang").Length > 0) { Assert.Skip("The application is already running — this check keeps out of someone else's run"); } return new Launch(StartProcess()); } /// Starts the application the way the user does. internal static Process StartProcess() { string path = ExecutablePath(); if (!File.Exists(path)) { Assert.Skip($"The application is not built: {path}"); } return Process.Start(new ProcessStartInfo(path) { UseShellExecute = true })!; } /// /// Waits for the settings window: by the time it appears the application /// has raised its whole cast. /// internal IntPtr WaitForWindow() { DateTime deadline = DateTime.UtcNow + StartTimeout; while (DateTime.UtcNow < deadline) { Process.Refresh(); if (Process.HasExited) { Assert.Fail($"The application exited while starting with code {Process.ExitCode}"); } if (Process.MainWindowHandle != IntPtr.Zero) { return Process.MainWindowHandle; } Thread.Sleep(100); } Assert.Fail("The settings window never appeared"); return IntPtr.Zero; } /// /// Whether there is a desktop here to show a window on. A service gets a /// window station of its own — "Service-0x0-3e7$" and the like: a window can /// be created there, yet nothing shows it. Only "WinSta0" is the interactive one. /// private static bool HasInteractiveDesktop() { IntPtr station = GetProcessWindowStation(); if (station == IntPtr.Zero) { return false; } var name = new StringBuilder(256); return GetUserObjectInformation(station, UOI_NAME, name, name.Capacity * sizeof(char), out _) && name.ToString().Equals(InteractiveWindowStation, StringComparison.OrdinalIgnoreCase); } private static string ExecutablePath() { string configured = Assembly.GetExecutingAssembly() .GetCustomAttributes() .Single(attribute => attribute.Key == "CursorLangExecutable") .Value!; return Path.GetFullPath(configured); } public void Dispose() { try { if (!Process.HasExited) { // The polite way first — that way the app gets to save its settings if (!Process.CloseMainWindow() || !Process.WaitForExit(ExitTimeout)) { Process.Kill(entireProcessTree: true); Process.WaitForExit(ExitTimeout); } } } catch (InvalidOperationException) { // The process has already ended on its own } finally { Process.Dispose(); } } } }