added tests to project
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CursorLang.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The application as a whole: the start, the single instance and the exit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>A started application that shuts down together with the check.</summary>
|
||||
private sealed class Launch : IDisposable
|
||||
{
|
||||
private Launch(Process process) => Process = process;
|
||||
|
||||
internal Process Process { get; }
|
||||
|
||||
/// <summary>Starts the application first — making sure the place is free.</summary>
|
||||
internal static Launch Start()
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>Starts the application the way the user does.</summary>
|
||||
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 })!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the settings window: by the time it appears the application
|
||||
/// has raised its whole cast.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
private static string ExecutablePath()
|
||||
{
|
||||
string configured = Assembly.GetExecutingAssembly()
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user