fixed tests
Release / release (push) Successful in 8m58s

This commit is contained in:
2026-08-11 16:34:28 +05:00
parent 3bf2018a62
commit 08192d317e
4 changed files with 86 additions and 23 deletions
+39
View File
@@ -1,6 +1,8 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace CursorLang.Tests; namespace CursorLang.Tests;
@@ -14,6 +16,9 @@ namespace CursorLang.Tests;
/// ///
/// If the application is already running in this session, the checks skip /// If the application is already running in this session, the checks skip
/// themselves: meddling with someone else's running instance is not their business. /// 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.
/// </remarks> /// </remarks>
public sealed class EndToEndTests public sealed class EndToEndTests
{ {
@@ -59,6 +64,16 @@ public sealed class EndToEndTests
/// <summary>A started application that shuts down together with the check.</summary> /// <summary>A started application that shuts down together with the check.</summary>
private sealed class Launch : IDisposable 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; private Launch(Process process) => Process = process;
internal Process Process { get; } internal Process Process { get; }
@@ -66,6 +81,11 @@ public sealed class EndToEndTests
/// <summary>Starts the application first — making sure the place is free.</summary> /// <summary>Starts the application first — making sure the place is free.</summary>
internal static Launch Start() 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) if (Process.GetProcessesByName("CursorLang").Length > 0)
{ {
Assert.Skip("The application is already running — this check keeps out of someone else's run"); Assert.Skip("The application is already running — this check keeps out of someone else's run");
@@ -116,6 +136,25 @@ public sealed class EndToEndTests
return IntPtr.Zero; return IntPtr.Zero;
} }
/// <summary>
/// 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.
/// </summary>
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() private static string ExecutablePath()
{ {
string configured = Assembly.GetExecutingAssembly() string configured = Assembly.GetExecutingAssembly()
@@ -124,9 +124,7 @@ public sealed class LayoutPopupWindowTests
{ {
var settings = new AppSettings var settings = new AppSettings
{ {
PlacementMode = PopupPlacementMode.FixedPoint, PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = position, ScreenMargin = 24,
ScreenPosition = position,
ScreenMargin = 24,
}; };
using var popup = Popup.Create(settings); using var popup = Popup.Create(settings);
@@ -160,9 +158,7 @@ public sealed class LayoutPopupWindowTests
{ {
var settings = new AppSettings var settings = new AppSettings
{ {
PlacementMode = PopupPlacementMode.AtCursor, PlacementMode = PopupPlacementMode.AtCursor, CursorSide = AnchorSide.BottomRight, CursorOffset = 16,
CursorSide = AnchorSide.BottomRight,
CursorOffset = 16,
}; };
using var popup = Popup.Create(settings); using var popup = Popup.Create(settings);
@@ -213,23 +209,48 @@ public sealed class LayoutPopupWindowTests
{ {
var settings = new AppSettings var settings = new AppSettings
{ {
PlacementMode = PopupPlacementMode.AtCaret, PlacementMode = PopupPlacementMode.AtCaret, CaretSide = AnchorSide.BottomRight, CaretOffset = 8,
CaretSide = AnchorSide.BottomRight,
CaretOffset = 8,
}; };
using var popup = Popup.Create(settings); using var popup = Popup.Create(settings);
popup.ViewModel.ShortName = "RU"; popup.ViewModel.ShortName = "RU";
PopupWindowNative.Point before = PopupWindowNative.GetCursorPosition(); PopupWindowNative.Point before = default;
for (int attempt = 0; attempt < 10; attempt++)
{
if (CaretNative.TryGetCaretRect() is not null)
{
Assert.Skip("The foreground window reported a caret of its own");
}
before = PopupWindowNative.GetCursorPosition();
popup.Window.ShowPopup(); popup.Window.ShowPopup();
PopupWindowNative.Point after = PopupWindowNative.GetCursorPosition();
if (before.X == after.X && before.Y == after.Y)
{
break;
}
if (attempt == 9)
{
Assert.Skip("The cursor kept moving the whole time");
}
}
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value; PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
double scale = PopupWindowNative.GetScaleAt(before);
// A gentle check: a window holding the input focus may still have a caret PopupWindowNative.Point expected = PopupLayout.NearAnchor(
Assert.True(bounds.Right > bounds.Left); PopupLayout.AsAnchor(before),
Assert.True(bounds.Bottom > bounds.Top); settings.CaretSide,
Assert.NotEqual(default, before); PopupLayout.ToPixels(settings.CaretOffset, scale),
bounds.Right - bounds.Left,
bounds.Bottom - bounds.Top);
Assert.Equal(expected.X, bounds.Left);
Assert.Equal(expected.Y, bounds.Top);
}); });
} }
@@ -266,8 +287,7 @@ public sealed class LayoutPopupWindowTests
{ {
var settings = new AppSettings var settings = new AppSettings
{ {
PlacementMode = PopupPlacementMode.FixedPoint, PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = ScreenPosition.TopLeft,
ScreenPosition = ScreenPosition.TopLeft,
}; };
using var popup = Popup.Create(settings); using var popup = Popup.Create(settings);
+5 -3
View File
@@ -147,9 +147,11 @@ dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage
Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём — Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём —
.NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка .NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка
остаётся текстовой заглушкой и сборка на ней падает. Работать runner должен остаётся текстовой заглушкой и сборка на ней падает. Работать runner лучше
в интерактивном сеансе рабочего стола тесты поднимают настоящие окна, в интерактивном сеансе рабочего стола: тесты поднимают настоящие окна, и те
а службе в нулевом сеансе ждать нечего. проверки, которым нужен свой рабочий стол — сквозные, а также те, что просят
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
сеансе, пропускают себя: показать окно там негде.
Пакет, который несёт релиз, загружается в Partner Center как есть. Identity Пакет, который несёт релиз, загружается в Partner Center как есть. Identity
берётся из переменных репозитория, а если те не заданы — из значений по берётся из переменных репозитория, а если те не заданы — из значений по
+5 -3
View File
@@ -145,9 +145,11 @@ say in it.
Both pipelines ask for a Windows runner labelled `windows-x64` with the .NET 10 Both pipelines ask for a Windows runner labelled `windows-x64` with the .NET 10
SDK and git-lfs on it. The checkout pulls LFS files: without them the icon is a SDK and git-lfs on it. The checkout pulls LFS files: without them the icon is a
text pointer and the build fails on it. The runner has to work in an interactive text pointer and the build fails on it. The runner is better off working in an
desktop session the tests raise real windows, and a service in session 0 has interactive desktop session: the tests raise real windows, and the checks that
nothing for them to wait for. need a desktop of their own — the end-to-end ones, and those that ask for the
foreground window or the caret — skip themselves on a runner that lives as a
service in session 0, where there is no desktop to show a window on.
The package the release carries goes to Partner Center as it is. The identity The package the release carries goes to Partner Center as it is. The identity
comes from repository variables and falls back to the defaults of the script when comes from repository variables and falls back to the defaults of the script when