@@ -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
@@ -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
|
||||||
берётся из переменных репозитория, а если те не заданы — из значений по
|
берётся из переменных репозитория, а если те не заданы — из значений по
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user