From 08192d317e18d6f56de4d8a888c198df195cd3de Mon Sep 17 00:00:00 2001 From: Aleksandr Neychev Date: Tue, 11 Aug 2026 16:34:28 +0500 Subject: [PATCH] fixed tests --- CursorLang.Tests/EndToEndTests.cs | 39 ++++++++++++++ .../Views/LayoutPopupWindowTests.cs | 54 +++++++++++++------ README.RU.md | 8 +-- README.md | 8 +-- 4 files changed, 86 insertions(+), 23 deletions(-) diff --git a/CursorLang.Tests/EndToEndTests.cs b/CursorLang.Tests/EndToEndTests.cs index 1af88e6..a5d3bde 100644 --- a/CursorLang.Tests/EndToEndTests.cs +++ b/CursorLang.Tests/EndToEndTests.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using System.IO; using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; namespace CursorLang.Tests; @@ -14,6 +16,9 @@ namespace CursorLang.Tests; /// /// 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 { @@ -59,6 +64,16 @@ public sealed class EndToEndTests /// 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; } @@ -66,6 +81,11 @@ public sealed class EndToEndTests /// 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"); @@ -116,6 +136,25 @@ public sealed class EndToEndTests 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() diff --git a/CursorLang.Tests/Views/LayoutPopupWindowTests.cs b/CursorLang.Tests/Views/LayoutPopupWindowTests.cs index 1a4b8ee..e94576a 100644 --- a/CursorLang.Tests/Views/LayoutPopupWindowTests.cs +++ b/CursorLang.Tests/Views/LayoutPopupWindowTests.cs @@ -124,9 +124,7 @@ public sealed class LayoutPopupWindowTests { var settings = new AppSettings { - PlacementMode = PopupPlacementMode.FixedPoint, - ScreenPosition = position, - ScreenMargin = 24, + PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = position, ScreenMargin = 24, }; using var popup = Popup.Create(settings); @@ -160,9 +158,7 @@ public sealed class LayoutPopupWindowTests { var settings = new AppSettings { - PlacementMode = PopupPlacementMode.AtCursor, - CursorSide = AnchorSide.BottomRight, - CursorOffset = 16, + PlacementMode = PopupPlacementMode.AtCursor, CursorSide = AnchorSide.BottomRight, CursorOffset = 16, }; using var popup = Popup.Create(settings); @@ -213,23 +209,48 @@ public sealed class LayoutPopupWindowTests { var settings = new AppSettings { - PlacementMode = PopupPlacementMode.AtCaret, - CaretSide = AnchorSide.BottomRight, - CaretOffset = 8, + PlacementMode = PopupPlacementMode.AtCaret, CaretSide = AnchorSide.BottomRight, CaretOffset = 8, }; using var popup = Popup.Create(settings); popup.ViewModel.ShortName = "RU"; - PopupWindowNative.Point before = PopupWindowNative.GetCursorPosition(); - popup.Window.ShowPopup(); + 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(); + 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; + double scale = PopupWindowNative.GetScaleAt(before); - // A gentle check: a window holding the input focus may still have a caret - Assert.True(bounds.Right > bounds.Left); - Assert.True(bounds.Bottom > bounds.Top); - Assert.NotEqual(default, before); + PopupWindowNative.Point expected = PopupLayout.NearAnchor( + PopupLayout.AsAnchor(before), + settings.CaretSide, + 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 { - PlacementMode = PopupPlacementMode.FixedPoint, - ScreenPosition = ScreenPosition.TopLeft, + PlacementMode = PopupPlacementMode.FixedPoint, ScreenPosition = ScreenPosition.TopLeft, }; using var popup = Popup.Create(settings); diff --git a/README.RU.md b/README.RU.md index d61735a..8f30840 100644 --- a/README.RU.md +++ b/README.RU.md @@ -147,9 +147,11 @@ dotnet test --collect:"XPlat Code Coverage" --settings CursorLang.Tests\coverage Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём — .NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка -остаётся текстовой заглушкой и сборка на ней падает. Работать runner должен -в интерактивном сеансе рабочего стола — тесты поднимают настоящие окна, -а службе в нулевом сеансе ждать нечего. +остаётся текстовой заглушкой и сборка на ней падает. Работать runner лучше +в интерактивном сеансе рабочего стола: тесты поднимают настоящие окна, и те +проверки, которым нужен свой рабочий стол — сквозные, а также те, что просят +окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом +сеансе, пропускают себя: показать окно там негде. Пакет, который несёт релиз, загружается в Partner Center как есть. Identity берётся из переменных репозитория, а если те не заданы — из значений по diff --git a/README.md b/README.md index 42c057d..02b2b21 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,11 @@ say in it. 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 -text pointer and the build fails on it. The runner has to work in an interactive -desktop session — the tests raise real windows, and a service in session 0 has -nothing for them to wait for. +text pointer and the build fails on it. The runner is better off working in an +interactive desktop session: the tests raise real windows, and the checks that +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 comes from repository variables and falls back to the defaults of the script when