Skip to content

Commit 2ffe6f4

Browse files
committed
Start adding tests for package management
1 parent 0d894dc commit 2ffe6f4

File tree

6 files changed

+344
-0
lines changed

6 files changed

+344
-0
lines changed

src/AppInstallerCLI.sln

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "targets", "targets", "{A0B4
209209
EndProject
210210
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PureLib", "PureLib\PureLib.vcxitems", "{A33223D2-550B-4D99-A53D-488B1F68683E}"
211211
EndProject
212+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetTestCommon", "WinGetTestCommon\WinGetTestCommon.csproj", "{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}"
213+
EndProject
214+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
215+
EndProject
212216
Global
213217
GlobalSection(SolutionConfigurationPlatforms) = preSolution
214218
Debug|ARM64 = Debug|ARM64
@@ -966,24 +970,55 @@ Global
966970
{9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x64.Build.0 = ReleaseStatic|Any CPU
967971
{9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x86.ActiveCfg = ReleaseStatic|Any CPU
968972
{9406322E-6272-487E-902A-9953889719EA}.ReleaseStatic|x86.Build.0 = ReleaseStatic|Any CPU
973+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|ARM64.ActiveCfg = Debug|x64
974+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|ARM64.Build.0 = Debug|x64
975+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|x64.ActiveCfg = Debug|x64
976+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|x64.Build.0 = Debug|x64
977+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|x86.ActiveCfg = Debug|x86
978+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Debug|x86.Build.0 = Debug|x86
979+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|ARM64.ActiveCfg = Release|x64
980+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|ARM64.Build.0 = Release|x64
981+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|x64.ActiveCfg = Release|x64
982+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|x64.Build.0 = Release|x64
983+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|x86.ActiveCfg = Release|x86
984+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Fuzzing|x86.Build.0 = Release|x86
985+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|ARM64.ActiveCfg = Release|x64
986+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|ARM64.Build.0 = Release|x64
987+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|x64.ActiveCfg = Release|x64
988+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|x64.Build.0 = Release|x64
989+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|x86.ActiveCfg = Release|x86
990+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.Release|x86.Build.0 = Release|x86
991+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|ARM64.ActiveCfg = Release|x64
992+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|ARM64.Build.0 = Release|x64
993+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|x64.ActiveCfg = Release|x64
994+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|x64.Build.0 = Release|x64
995+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|x86.ActiveCfg = Release|x86
996+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A}.ReleaseStatic|x86.Build.0 = Release|x86
969997
EndGlobalSection
970998
GlobalSection(SolutionProperties) = preSolution
971999
HideSolutionNode = FALSE
9721000
EndGlobalSection
9731001
GlobalSection(NestedProjects) = preSolution
1002+
{89B1AAB4-2BBC-4B65-9ED7-A01D5CF88230} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
1003+
{6CB84692-5994-407D-B9BD-9216AF77FE83} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
9741004
{6E36DDD7-1602-474E-B1D7-D0A7E1D5AD86} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
1005+
{3C0269FA-E582-4CA7-9E33-3881A005CA0C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
1006+
{3E2CBA31-CEBA-4D63-BF52-49C0718E19EA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
1007+
{C1624B2F-2BF6-4E28-92FA-1BF85C6B62A8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
9751008
{3B8466CF-4FDD-4329-9C80-91321C4AAC99} = {EA8CD934-0702-4911-A2C5-A40600E616DE}
9761009
{1622DA16-914F-4F57-A259-D5169003CC8C} = {6D7776A8-42FE-46DD-B0F8-712F35EA0C79}
9771010
{3BAF989F-7F65-465B-ACE8-BAFE42D1017E} = {EA8CD934-0702-4911-A2C5-A40600E616DE}
9781011
{7D05F64D-CE5A-42AA-A2C1-E91458F061CF} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
9791012
{952B513F-8A00-4D74-9271-925AFB3C6252} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
9801013
{2ACDE176-F13F-42FA-8159-C34FA3D37837} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
9811014
{1A47951F-5C7A-4D6D-BB5F-D77484437940} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
1015+
{68808357-902B-406C-8C19-E8E26A69DE8A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
9821016
{409CD681-22A4-469D-88AE-CB5E4836E07A} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
9831017
{B0BBBD92-943B-408F-B2B2-DBBAB4A22D23} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
9841018
{463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9}
9851019
{31ED69A8-5310-45A9-953F-56C351D2C3E1} = {60618CAC-2995-4DF9-9914-45C6FC02C995}
9861020
{8E43F982-40D5-4DF1-9044-C08047B5F43B} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
1021+
{EE43C990-7789-4A60-B077-BF0ED3D093A1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
9871022
{1F56BECB-D65D-4BBA-8788-6671B251392A} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9}
9881023
{167F634B-A3AD-494E-8E67-B888103E35FF} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9}
9891024
{C54F80ED-B736-49B0-9BD3-662F57024D01} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9}
@@ -993,6 +1028,7 @@ Global
9931028
{76B26B2C-602A-4AD0-9736-4162D3FCA92A} = {1A5D7A7D-5CB2-47D5-B40D-4E61CAEDC798}
9941029
{A0B4F808-B190-41C4-97CB-C8EA1932F84F} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7}
9951030
{A33223D2-550B-4D99-A53D-488B1F68683E} = {60618CAC-2995-4DF9-9914-45C6FC02C995}
1031+
{7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
9961032
EndGlobalSection
9971033
GlobalSection(ExtensibilityGlobals) = postSolution
9981034
SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857}

src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
6666
</ProjectReference>
6767
<ProjectReference Include="..\WinGetSourceCreator\WinGetSourceCreator.csproj" />
68+
<ProjectReference Include="..\WinGetTestCommon\WinGetTestCommon.csproj" />
6869
<ProjectReference Include="..\WinGetUtil\WinGetUtil.vcxproj">
6970
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
7071
</ProjectReference>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// -----------------------------------------------------------------------------
2+
// <copyright file="Shutdown.cs" company="Microsoft Corporation">
3+
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
4+
// </copyright>
5+
// -----------------------------------------------------------------------------
6+
7+
namespace AppInstallerCLIE2ETests.Interop
8+
{
9+
using System;
10+
using Microsoft.Management.Deployment.Projection;
11+
using NUnit.Framework;
12+
using WinGetTestCommon;
13+
14+
/// <summary>
15+
/// Shutdown testing.
16+
/// </summary>
17+
[TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.OutOfProcess), Category = nameof(InstanceInitializersSource.OutOfProcess))]
18+
public class Shutdown : BaseInterop
19+
{
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="Shutdown"/> class.
22+
/// </summary>
23+
/// <param name="initializer">Initializer.</param>
24+
public Shutdown(IInstanceInitializer initializer)
25+
: base(initializer)
26+
{
27+
}
28+
29+
/// <summary>
30+
/// Checks that shutdown will proceed even though an object is active.
31+
/// </summary>
32+
[Test]
33+
public void NoActiveOperations()
34+
{
35+
var packageManager = this.TestFactory.CreatePackageManager();
36+
37+
var servers = WinGetServerInstance.GetInstances();
38+
Assert.AreEqual(1, servers.Count);
39+
40+
var server = servers[0];
41+
Assert.IsTrue(server.HasWindow);
42+
43+
// This is the call pattern from Windows
44+
this.SendMessageAndLog(server, WindowMessage.QueryEndSession);
45+
this.SendMessageAndLog(server, WindowMessage.EndSession);
46+
this.SendMessageAndLog(server, WindowMessage.Close);
47+
48+
Assert.IsTrue(server.Process.HasExited);
49+
Assert.Throws(packageManager.Version);
50+
}
51+
52+
private void SendMessageAndLog(WinGetServerInstance server, WindowMessage message)
53+
{
54+
TestContext.Out.WriteLine($"Sending message {message} to process {server.Process.Id}...");
55+
try
56+
{
57+
if (server.SendMessage(message))
58+
{
59+
TestContext.Out.WriteLine("... succeeded.");
60+
}
61+
else
62+
{
63+
TestContext.Out.WriteLine("... failed.");
64+
}
65+
}
66+
catch (Exception e)
67+
{
68+
TestContext.Out.WriteLine($"... had exception: {e.Message}");
69+
}
70+
}
71+
}
72+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// -----------------------------------------------------------------------------
2+
// <copyright file="WinGetServerInstance.cs" company="Microsoft Corporation">
3+
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
4+
// </copyright>
5+
// -----------------------------------------------------------------------------
6+
7+
namespace WinGetTestCommon
8+
{
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Diagnostics;
12+
using System.Runtime.InteropServices;
13+
14+
/// <summary>
15+
/// Represents an instance of a Windows Package Manager (WinGet) server.
16+
/// </summary>
17+
public class WinGetServerInstance
18+
{
19+
/// <summary>
20+
/// The name of the executable for the COM server.
21+
/// </summary>
22+
public const string ServerExecutableName = "WindowsPackageManagerServer.exe";
23+
24+
/// <summary>
25+
/// The package family name for the development package.
26+
/// </summary>
27+
public const string DevelopmentPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe";
28+
29+
/// <summary>
30+
/// The window name for the COM server message window.
31+
/// </summary>
32+
public const string TargetWindowName = "WingetMessageOnlyWindow";
33+
34+
/// <summary>
35+
/// Gets the process for the server.
36+
/// </summary>
37+
public required Process Process { get; init; }
38+
39+
/// <summary>
40+
/// Gets a value indicating whether the current server has an associated window.
41+
/// </summary>
42+
public bool HasWindow
43+
{
44+
get
45+
{
46+
return EnumerateWindowHandles(TargetWindowName).Count > 0;
47+
}
48+
}
49+
50+
/// <summary>
51+
/// Sends a specified message to a window.
52+
/// </summary>
53+
/// <param name="message">The message to be sent to the window.</param>
54+
/// <returns>True to indicate that the message was sent and processed within the timeout; false otherwise.</returns>
55+
public bool SendMessage(WindowMessage message)
56+
{
57+
const int TRUE = 0x1;
58+
const int ENDSESSION_CLOSEAPP = 0x1;
59+
const uint SMTO_ABORTIFHUNG = 0x0002;
60+
const uint TIMEOUT_MS = 5000;
61+
62+
var windowHandles = EnumerateWindowHandles(TargetWindowName);
63+
64+
if (windowHandles.Count > 1)
65+
{
66+
throw new InvalidOperationException($"Target process has more than one window named `{TargetWindowName}`");
67+
}
68+
69+
foreach (var hWnd in windowHandles)
70+
{
71+
IntPtr result;
72+
bool success;
73+
switch (message)
74+
{
75+
case WindowMessage.Close:
76+
success = SendMessageTimeout(hWnd, (uint)message, IntPtr.Zero, IntPtr.Zero, SMTO_ABORTIFHUNG, TIMEOUT_MS, out result) != IntPtr.Zero;
77+
break;
78+
case WindowMessage.QueryEndSession:
79+
success = SendMessageTimeout(hWnd, (uint)message, IntPtr.Zero, (IntPtr)ENDSESSION_CLOSEAPP, SMTO_ABORTIFHUNG, TIMEOUT_MS, out result) != IntPtr.Zero;
80+
break;
81+
case WindowMessage.EndSession:
82+
success = SendMessageTimeout(hWnd, (uint)message, (IntPtr)TRUE, (IntPtr)ENDSESSION_CLOSEAPP, SMTO_ABORTIFHUNG, TIMEOUT_MS, out result) != IntPtr.Zero;
83+
break;
84+
default:
85+
throw new NotImplementedException("Unexpected window message");
86+
}
87+
88+
return success;
89+
}
90+
91+
return false;
92+
}
93+
94+
/// <summary>
95+
/// Retrieves an array of all available WinGet server instances.
96+
/// </summary>
97+
/// <returns>
98+
/// An array of <see cref="WinGetServerInstance"/> objects representing the available server instances.
99+
/// The array will be empty if no instances are available.
100+
/// </returns>
101+
public static List<WinGetServerInstance> GetInstances()
102+
{
103+
Process[] processes = Process.GetProcessesByName(ServerExecutableName);
104+
List<WinGetServerInstance> result = new List<WinGetServerInstance>();
105+
106+
foreach (Process process in processes)
107+
{
108+
try
109+
{
110+
string? familyName = GetProcessPackageFamilyName(process);
111+
if (familyName == DevelopmentPackageFamilyName)
112+
{
113+
result.Add(new WinGetServerInstance { Process = process });
114+
}
115+
}
116+
catch
117+
{
118+
// Ignore processes that we can't access or that aren't packaged
119+
}
120+
}
121+
122+
return result;
123+
}
124+
125+
private static string? GetProcessPackageFamilyName(Process process)
126+
{
127+
const int ERROR_INSUFFICIENT_BUFFER = 122;
128+
int length = 0;
129+
int result = GetPackageFamilyName(process.Handle, ref length, null);
130+
if (result == ERROR_INSUFFICIENT_BUFFER)
131+
{
132+
var sb = new System.Text.StringBuilder(length);
133+
result = GetPackageFamilyName(process.Handle, ref length, sb);
134+
if (result == 0)
135+
{
136+
return sb.ToString();
137+
}
138+
}
139+
return null;
140+
}
141+
142+
private List<IntPtr> EnumerateWindowHandles(string windowName)
143+
{
144+
List<IntPtr> windowHandles = new List<IntPtr>();
145+
int processId = Process.Id;
146+
147+
bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam)
148+
{
149+
GetWindowThreadProcessId(hWnd, out int windowProcessId);
150+
if (windowProcessId == processId)
151+
{
152+
// Get the window title
153+
var sb = new System.Text.StringBuilder(256);
154+
int length = GetWindowText(hWnd, sb, sb.Capacity);
155+
if (length > 0 && sb.ToString() == windowName)
156+
{
157+
windowHandles.Add(hWnd);
158+
}
159+
}
160+
return true;
161+
}
162+
163+
EnumWindows(EnumWindowsProc, IntPtr.Zero);
164+
return windowHandles;
165+
}
166+
167+
[DllImport("user32.dll")]
168+
private static extern bool EnumWindows(EnumWindowsProcDelegate lpEnumFunc, IntPtr lParam);
169+
170+
private delegate bool EnumWindowsProcDelegate(IntPtr hWnd, IntPtr lParam);
171+
172+
[DllImport("user32.dll", SetLastError = true)]
173+
private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
174+
175+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
176+
private static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
177+
178+
[DllImport("user32.dll", SetLastError = true)]
179+
private static extern IntPtr SendMessageTimeout(
180+
IntPtr hWnd,
181+
uint Msg,
182+
IntPtr wParam,
183+
IntPtr lParam,
184+
uint fuFlags,
185+
uint uTimeout,
186+
out IntPtr lpdwResult
187+
);
188+
189+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
190+
private static extern int GetPackageFamilyName(
191+
IntPtr hProcess,
192+
ref int packageFamilyNameLength,
193+
System.Text.StringBuilder? packageFamilyName
194+
);
195+
}
196+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0-windows</TargetFramework>
5+
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\WinGetTestCommon\</OutDir>
6+
<Platforms>x64;x86</Platforms>
7+
<OutputType>Library</OutputType>
8+
<Nullable>enable</Nullable>
9+
</PropertyGroup>
10+
</Project>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// -----------------------------------------------------------------------------
2+
// <copyright file="WindowMessage.cs" company="Microsoft Corporation">
3+
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
4+
// </copyright>
5+
// -----------------------------------------------------------------------------
6+
7+
namespace WinGetTestCommon
8+
{
9+
/// <summary>
10+
/// Represents the Windows messages that can be sent or received by a window.
11+
/// </summary>
12+
public enum WindowMessage
13+
{
14+
/// <summary>
15+
/// WM_CLOSE
16+
/// </summary>
17+
Close = 0x0010,
18+
19+
/// <summary>
20+
/// WM_QUERYENDSESSION
21+
/// </summary>
22+
QueryEndSession = 0x0011,
23+
24+
/// <summary>
25+
/// WM_ENDSESSION
26+
/// </summary>
27+
EndSession = 0x0016,
28+
}
29+
}

0 commit comments

Comments
 (0)