diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
index bbc10d5ddd..3b3ee8888a 100644
--- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
+++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
@@ -37,6 +37,7 @@
+
diff --git a/src/UniGetUI.Core.SecureSettings/SecureSettings.cs b/src/UniGetUI.Core.SecureSettings/SecureSettings.cs
index b5063de47c..eedce59ee9 100644
--- a/src/UniGetUI.Core.SecureSettings/SecureSettings.cs
+++ b/src/UniGetUI.Core.SecureSettings/SecureSettings.cs
@@ -6,6 +6,8 @@ namespace UniGetUI.Core.SettingsEngine.SecureSettings;
public static class SecureSettings
{
+ public static string? TEST_SecureSettingsRootOverride { private get; set; }
+
// Various predefined secure settings keys
public enum K
{
@@ -140,6 +142,11 @@ public static int ApplyForUser(string username, string setting, bool enable)
private static string GetSecureSettingsRoot()
{
+ if (TEST_SecureSettingsRootOverride is not null)
+ {
+ return TEST_SecureSettingsRootOverride;
+ }
+
if (OperatingSystem.IsWindows())
{
return Path.Join(
diff --git a/src/UniGetUI.Core.Settings.Tests/SecureSettingsTests.cs b/src/UniGetUI.Core.Settings.Tests/SecureSettingsTests.cs
new file mode 100644
index 0000000000..e19fe06f70
--- /dev/null
+++ b/src/UniGetUI.Core.Settings.Tests/SecureSettingsTests.cs
@@ -0,0 +1,111 @@
+using System.Reflection;
+using UniGetUI.Core.Tools;
+using SecureSettingsStore = UniGetUI.Core.SettingsEngine.SecureSettings.SecureSettings;
+
+namespace UniGetUI.Core.SettingsEngine.Tests;
+
+public sealed class SecureSettingsTests : IDisposable
+{
+ private readonly string _testRoot;
+
+ public SecureSettingsTests()
+ {
+ _testRoot = Path.Combine(Path.GetTempPath(), $"UniGetUI-SecureSettingsTests-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(_testRoot);
+ SecureSettingsStore.TEST_SecureSettingsRootOverride = _testRoot;
+ ClearSecureSettingsCache();
+ }
+
+ public void Dispose()
+ {
+ ClearSecureSettingsCache();
+ SecureSettingsStore.TEST_SecureSettingsRootOverride = null;
+
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, true);
+ }
+ }
+
+ [Theory]
+ [InlineData(SecureSettingsStore.K.AllowCLIArguments, "AllowCLIArguments")]
+ [InlineData(SecureSettingsStore.K.AllowImportingCLIArguments, "AllowImportingCLIArguments")]
+ [InlineData(SecureSettingsStore.K.AllowPrePostOpCommand, "AllowPrePostInstallCommands")]
+ [InlineData(SecureSettingsStore.K.AllowImportPrePostOpCommands, "AllowImportingPrePostInstallCommands")]
+ [InlineData(SecureSettingsStore.K.ForceUserGSudo, "ForceUserGSudo")]
+ [InlineData(SecureSettingsStore.K.AllowCustomManagerPaths, "AllowCustomManagerPaths")]
+ public void ResolveKey_ReturnsExpectedMappings(SecureSettingsStore.K key, string expected)
+ {
+ Assert.Equal(expected, SecureSettingsStore.ResolveKey(key));
+ }
+
+ [Fact]
+ public void ResolveKey_ThrowsForUnsetAndUnknownKeys()
+ {
+ Assert.Throws(() =>
+ SecureSettingsStore.ResolveKey(SecureSettingsStore.K.Unset)
+ );
+ Assert.Throws(() =>
+ SecureSettingsStore.ResolveKey((SecureSettingsStore.K)999)
+ );
+ }
+
+ [Fact]
+ public void Get_ReturnsFalseWhenSettingDoesNotExist()
+ {
+ Assert.False(SecureSettingsStore.Get(SecureSettingsStore.K.AllowCLIArguments));
+ Assert.False(Directory.Exists(GetCurrentUserSettingsDirectory()));
+ }
+
+ [Fact]
+ public void ApplyForUser_CreatesAndRemovesSanitizedFile()
+ {
+ const string username = "test:user?";
+ const string setting = "settinginvalid|chars";
+
+ Assert.Equal(0, SecureSettingsStore.ApplyForUser(username, setting, true));
+ Assert.True(File.Exists(GetSettingsFilePath(username, setting)));
+
+ Assert.Equal(0, SecureSettingsStore.ApplyForUser(username, setting, false));
+ Assert.False(File.Exists(GetSettingsFilePath(username, setting)));
+ }
+
+ [Fact]
+ public void Get_RefreshesCachedValueAfterApplyForUserWrites()
+ {
+ string username = Environment.UserName;
+ string setting = SecureSettingsStore.ResolveKey(SecureSettingsStore.K.AllowCLIArguments);
+
+ Assert.False(SecureSettingsStore.Get(SecureSettingsStore.K.AllowCLIArguments));
+
+ Assert.Equal(0, SecureSettingsStore.ApplyForUser(username, setting, true));
+ Assert.True(File.Exists(GetSettingsFilePath(username, setting)));
+ Assert.True(SecureSettingsStore.Get(SecureSettingsStore.K.AllowCLIArguments));
+
+ Assert.Equal(0, SecureSettingsStore.ApplyForUser(username, setting, false));
+ Assert.False(File.Exists(GetSettingsFilePath(username, setting)));
+ Assert.False(SecureSettingsStore.Get(SecureSettingsStore.K.AllowCLIArguments));
+ }
+
+ private string GetCurrentUserSettingsDirectory() =>
+ Path.Combine(_testRoot, CoreTools.MakeValidFileName(Environment.UserName));
+
+ private string GetSettingsFilePath(string username, string setting) =>
+ Path.Combine(
+ _testRoot,
+ CoreTools.MakeValidFileName(username),
+ CoreTools.MakeValidFileName(setting)
+ );
+
+ private static void ClearSecureSettingsCache()
+ {
+ FieldInfo? cacheField = typeof(SecureSettingsStore).GetField(
+ "_cache",
+ BindingFlags.NonPublic | BindingFlags.Static
+ );
+ Assert.NotNull(cacheField);
+
+ var cache = Assert.IsType>(cacheField.GetValue(null));
+ cache.Clear();
+ }
+}
diff --git a/src/UniGetUI.Core.Settings.Tests/SettingsImportExportTests.cs b/src/UniGetUI.Core.Settings.Tests/SettingsImportExportTests.cs
new file mode 100644
index 0000000000..5dfa26833e
--- /dev/null
+++ b/src/UniGetUI.Core.Settings.Tests/SettingsImportExportTests.cs
@@ -0,0 +1,83 @@
+using System.Text.Json;
+using UniGetUI.Core.Data;
+
+namespace UniGetUI.Core.SettingsEngine.Tests;
+
+public sealed class SettingsImportExportTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ Path.GetTempPath(),
+ nameof(SettingsImportExportTests),
+ Guid.NewGuid().ToString("N")
+ );
+
+ public SettingsImportExportTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Settings.ResetSettings();
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ExportToStringJson_ExcludesSensitiveFiles()
+ {
+ Settings.Set(Settings.K.FreshBoolSetting, true);
+ Settings.SetValue(Settings.K.FreshValue, "configured");
+ File.WriteAllText(Path.Combine(CoreData.UniGetUIUserConfigurationDirectory, "TelemetryClientToken"), "secret");
+ File.WriteAllText(Path.Combine(CoreData.UniGetUIUserConfigurationDirectory, "CurrentSessionToken"), "secret");
+
+ var exported = JsonSerializer.Deserialize>(Settings.ExportToString_JSON());
+
+ Assert.NotNull(exported);
+ Assert.Contains(Settings.ResolveKey(Settings.K.FreshBoolSetting), exported.Keys);
+ Assert.Equal("configured", exported[Settings.ResolveKey(Settings.K.FreshValue)]);
+ Assert.DoesNotContain("TelemetryClientToken", exported.Keys);
+ Assert.DoesNotContain("CurrentSessionToken", exported.Keys);
+ }
+
+ [Fact]
+ public void ImportFromStringJson_ResetsExistingFilesAndReloadsCache()
+ {
+ Settings.Set(Settings.K.Test1, true);
+ Settings.SetValue(Settings.K.FreshValue, "old-value");
+
+ string importedJson = JsonSerializer.Serialize(
+ new Dictionary
+ {
+ [Settings.ResolveKey(Settings.K.Test2)] = "",
+ [Settings.ResolveKey(Settings.K.FreshValue)] = "new-value",
+ }
+ );
+
+ Settings.ImportFromString_JSON(importedJson);
+
+ Assert.False(File.Exists(Path.Combine(CoreData.UniGetUIUserConfigurationDirectory, Settings.ResolveKey(Settings.K.Test1))));
+ Assert.True(Settings.Get(Settings.K.Test2));
+ Assert.Equal("new-value", Settings.GetValue(Settings.K.FreshValue));
+ }
+
+ [Fact]
+ public void ImportFromFileJson_CopiesSourceWhenBackupLivesInSettingsDirectory()
+ {
+ Settings.SetValue(Settings.K.FreshValue, "before-import");
+ string exportPath = Path.Combine(CoreData.UniGetUIUserConfigurationDirectory, "settings-backup.json");
+
+ Settings.ExportToFile_JSON(exportPath);
+ Settings.SetValue(Settings.K.FreshValue, "after-export");
+
+ Settings.ImportFromFile_JSON(exportPath);
+
+ Assert.Equal("before-import", Settings.GetValue(Settings.K.FreshValue));
+ }
+}
diff --git a/src/UniGetUI.Core.Settings.Tests/TestAssembly.cs b/src/UniGetUI.Core.Settings.Tests/TestAssembly.cs
new file mode 100644
index 0000000000..217120083b
--- /dev/null
+++ b/src/UniGetUI.Core.Settings.Tests/TestAssembly.cs
@@ -0,0 +1,3 @@
+using Xunit;
+
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
diff --git a/src/UniGetUI.Core.Settings.Tests/UniGetUI.Core.Settings.Tests.csproj b/src/UniGetUI.Core.Settings.Tests/UniGetUI.Core.Settings.Tests.csproj
index 70793a2084..7fdd8a3d6f 100644
--- a/src/UniGetUI.Core.Settings.Tests/UniGetUI.Core.Settings.Tests.csproj
+++ b/src/UniGetUI.Core.Settings.Tests/UniGetUI.Core.Settings.Tests.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_ImportExport.cs b/src/UniGetUI.Core.Settings/SettingsEngine_ImportExport.cs
index 877c007c0c..f4be6a8b84 100644
--- a/src/UniGetUI.Core.Settings/SettingsEngine_ImportExport.cs
+++ b/src/UniGetUI.Core.Settings/SettingsEngine_ImportExport.cs
@@ -77,6 +77,11 @@ public static void ImportFromString_JSON(string jsonContent)
public static void ResetSettings()
{
+ booleanSettings.Clear();
+ valueSettings.Clear();
+ listSettings.Clear();
+ _dictionarySettings.Clear();
+
foreach (
string entry in Directory.EnumerateFiles(CoreData.UniGetUIUserConfigurationDirectory)
)
diff --git a/src/UniGetUI.Interface.Telemetry.Tests/TelemetryHandlerTests.cs b/src/UniGetUI.Interface.Telemetry.Tests/TelemetryHandlerTests.cs
new file mode 100644
index 0000000000..7f130ab1ac
--- /dev/null
+++ b/src/UniGetUI.Interface.Telemetry.Tests/TelemetryHandlerTests.cs
@@ -0,0 +1,327 @@
+using System.Net;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using UniGetUI.Core.Data;
+using UniGetUI.Core.Logging;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.PackageEngine.Classes.Manager;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.PackageClasses;
+
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
+
+namespace UniGetUI.Interface.Telemetry.Tests;
+
+public sealed class TelemetryHandlerTests : IDisposable
+{
+ private const string KnownInstallId =
+ "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
+
+ private readonly string _testRoot;
+ private readonly string _portableMarkerPath;
+ private readonly bool _originalWasDaemon;
+
+ public TelemetryHandlerTests()
+ {
+ _testRoot = Path.Combine(
+ AppContext.BaseDirectory,
+ nameof(TelemetryHandlerTests),
+ Guid.NewGuid().ToString("N")
+ );
+ _portableMarkerPath = Path.Combine(Environment.CurrentDirectory, "ForceUniGetUIPortable");
+ _originalWasDaemon = CoreData.WasDaemon;
+
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+
+ ClearSettingsCaches();
+ Settings.ResetSettings();
+ Settings.Set(Settings.K.DisableTelemetry, false);
+ Settings.Set(Settings.K.DisableWaitForInternetConnection, true);
+ Settings.SetValue(Settings.K.TelemetryClientToken, KnownInstallId);
+
+ TelemetryHandler.ResetTestState();
+ File.Delete(_portableMarkerPath);
+ CoreData.WasDaemon = false;
+ }
+
+ public void Dispose()
+ {
+ TelemetryHandler.ResetTestState();
+ ClearSettingsCaches();
+ CoreData.TEST_DataDirectoryOverride = null;
+ CoreData.WasDaemon = _originalWasDaemon;
+
+ if (File.Exists(_portableMarkerPath))
+ {
+ File.Delete(_portableMarkerPath);
+ }
+
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, true);
+ }
+ }
+
+ [Fact]
+ public async Task InitializeAsync_WithoutCredentials_DoesNotSendAndLogsOnce()
+ {
+ int sendCount = 0;
+ TelemetryHandler.TestSendAsyncOverride = _ =>
+ {
+ sendCount++;
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
+ };
+
+ int warningCountBefore = Logger
+ .GetLogs()
+ .Count(log => log.Content.Contains("OpenSearch credentials are not configured"));
+
+ await TelemetryHandler.InitializeAsync();
+ await TelemetryHandler.InitializeAsync();
+
+ int warningCountAfter = Logger
+ .GetLogs()
+ .Count(log => log.Content.Contains("OpenSearch credentials are not configured"));
+
+ Assert.Equal(0, sendCount);
+ Assert.Equal(warningCountBefore + 1, warningCountAfter);
+ }
+
+ [Fact]
+ public void ComputeActiveSettingsBitmask_IncludesDeterministicSettingsAndSpecialPaths()
+ {
+ Settings.Set(Settings.K.DisableAutoUpdateWingetUI, false);
+ Settings.Set(Settings.K.EnableUniGetUIBeta, true);
+ Settings.Set(Settings.K.DisableSystemTray, true);
+ Settings.Set(Settings.K.DisableNotifications, true);
+ Settings.Set(Settings.K.DisableAutoCheckforUpdates, false);
+ Settings.Set(Settings.K.AutomaticallyUpdatePackages, true);
+ Settings.Set(Settings.K.AskToDeleteNewDesktopShortcuts, false);
+ Settings.Set(Settings.K.EnablePackageBackup_LOCAL, false);
+ Settings.Set(Settings.K.DoCacheAdminRights, false);
+ Settings.Set(Settings.K.DoCacheAdminRightsForBatches, false);
+ Settings.Set(Settings.K.ForceLegacyBundledWinGet, false);
+ Settings.Set(Settings.K.UseSystemChocolatey, false);
+ File.WriteAllText(_portableMarkerPath, string.Empty);
+ CoreData.WasDaemon = true;
+
+ int activeSettings = TelemetryHandler.ComputeActiveSettingsBitmask();
+
+ Assert.Equal(12339, activeSettings);
+ }
+
+ [Fact]
+ public async Task InitializeAsync_SendsActivityPayloadWithRequiredFields()
+ {
+ Settings.Set(Settings.K.DisableAutoUpdateWingetUI, false);
+ Settings.Set(Settings.K.EnableUniGetUIBeta, true);
+ Settings.Set(Settings.K.DisableSystemTray, true);
+ Settings.Set(Settings.K.DisableNotifications, true);
+ Settings.Set(Settings.K.DisableAutoCheckforUpdates, false);
+ Settings.Set(Settings.K.AutomaticallyUpdatePackages, true);
+ File.WriteAllText(_portableMarkerPath, string.Empty);
+ CoreData.WasDaemon = true;
+
+ TelemetryHandler.Configure("telemetry-user", "telemetry-pass");
+ var captured = await CaptureRequestAsync(TelemetryHandler.InitializeAsync);
+
+ using var json = JsonDocument.Parse(captured.Body);
+ JsonElement root = json.RootElement;
+
+ Assert.Contains("activity_events", captured.RequestUri.AbsoluteUri);
+ Assert.Equal("Basic", captured.Authorization?.Scheme);
+ Assert.Equal(
+ Convert.ToBase64String(Encoding.UTF8.GetBytes("telemetry-user:telemetry-pass")),
+ captured.Authorization?.Parameter
+ );
+ Assert.NotEmpty(root.GetProperty("eventID").GetString());
+ Assert.NotEqual(default, root.GetProperty("eventDate").GetDateTime());
+ Assert.Equal(KnownInstallId, root.GetProperty("installID").GetString());
+ Assert.True(root.TryGetProperty("enabledManagers", out _));
+ Assert.True(root.TryGetProperty("foundManagers", out _));
+ Assert.Equal(12339, root.GetProperty("activeSettings").GetInt32());
+ Assert.Equal("UniGetUI", root.GetProperty("application").GetProperty("name").GetString());
+ Assert.Equal("NotApplicable", root.GetProperty("application").GetProperty("dataSource").GetString());
+ Assert.Equal("Free", root.GetProperty("application").GetProperty("pricing").GetString());
+ Assert.NotEmpty(root.GetProperty("platform").GetProperty("name").GetString());
+ }
+
+ [Fact]
+ public async Task InstallPackage_SendsPackagePayloadWithResultAndReferral()
+ {
+ TelemetryHandler.Configure("telemetry-user", "telemetry-pass");
+ var package = new Package(
+ "Telemetry Package",
+ "Telemetry.Package",
+ "1.0.0",
+ new NullSource("Telemetry Source"),
+ NullPackageManager.Instance
+ );
+
+ var captured = await CaptureRequestAsync(() =>
+ {
+ TelemetryHandler.InstallPackage(
+ package,
+ TEL_OP_RESULT.SUCCESS,
+ TEL_InstallReferral.FROM_BUNDLE
+ );
+ });
+
+ using var json = JsonDocument.Parse(captured.Body);
+ JsonElement root = json.RootElement;
+
+ Assert.Contains("package_events", captured.RequestUri.AbsoluteUri);
+ Assert.Equal("install", root.GetProperty("operation").GetString());
+ Assert.Equal(package.Id, root.GetProperty("packageId").GetString());
+ Assert.Equal(package.Manager.Name, root.GetProperty("managerName").GetString());
+ Assert.Equal(package.Source.Name, root.GetProperty("sourceName").GetString());
+ Assert.Equal(TEL_OP_RESULT.SUCCESS.ToString(), root.GetProperty("operationResult").GetString());
+ Assert.Equal(
+ TEL_InstallReferral.FROM_BUNDLE.ToString(),
+ root.GetProperty("eventSource").GetString()
+ );
+ Assert.Equal(KnownInstallId, root.GetProperty("installID").GetString());
+ }
+
+ [Fact]
+ public async Task PackageDetails_SendsEventSourceWithoutOperationResult()
+ {
+ TelemetryHandler.Configure("telemetry-user", "telemetry-pass");
+ var package = new Package(
+ "Telemetry Package",
+ "Telemetry.Package",
+ "1.0.0",
+ new NullSource("Telemetry Source"),
+ NullPackageManager.Instance
+ );
+
+ var captured = await CaptureRequestAsync(() =>
+ {
+ TelemetryHandler.PackageDetails(package, "DIRECT_LINK");
+ });
+
+ using var json = JsonDocument.Parse(captured.Body);
+ JsonElement root = json.RootElement;
+
+ Assert.Equal("details", root.GetProperty("operation").GetString());
+ Assert.Equal("DIRECT_LINK", root.GetProperty("eventSource").GetString());
+ Assert.False(root.TryGetProperty("operationResult", out _));
+ }
+
+ [Fact]
+ public async Task BundleOperations_SendExpectedRoutingPayloads()
+ {
+ TelemetryHandler.Configure("telemetry-user", "telemetry-pass");
+ var captured = await CaptureRequestsAsync(
+ expectedCount: 3,
+ trigger: () =>
+ {
+ TelemetryHandler.ImportBundle(BundleFormatType.JSON);
+ TelemetryHandler.ExportBundle(BundleFormatType.UBUNDLE);
+ TelemetryHandler.ExportBatch();
+ }
+ );
+
+ var actualRoutes = captured
+ .Select(request =>
+ {
+ using JsonDocument json = JsonDocument.Parse(request.Body);
+ JsonElement root = json.RootElement;
+ return (
+ Operation: root.GetProperty("operation").GetString(),
+ BundleType: root.GetProperty("bundleType").GetString()
+ );
+ })
+ .OrderBy(route => route.Operation, StringComparer.Ordinal)
+ .ThenBy(route => route.BundleType, StringComparer.Ordinal)
+ .ToArray();
+
+ Assert.Equal(
+ [
+ ("export", "PS1_SCRIPT"),
+ ("export", BundleFormatType.UBUNDLE.ToString()),
+ ("import", BundleFormatType.JSON.ToString()),
+ ],
+ actualRoutes
+ );
+ }
+
+ private static async Task CaptureRequestAsync(Func trigger)
+ {
+ TaskCompletionSource completionSource =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+ TelemetryHandler.TestSendAsyncOverride = async request =>
+ {
+ completionSource.TrySetResult(await CapturedRequest.CreateAsync(request));
+ return new HttpResponseMessage(HttpStatusCode.OK);
+ };
+
+ await trigger();
+ return await completionSource.Task.WaitAsync(TimeSpan.FromSeconds(5));
+ }
+
+ private static Task CaptureRequestAsync(Action trigger) =>
+ CaptureRequestAsync(() =>
+ {
+ trigger();
+ return Task.CompletedTask;
+ });
+
+ private static async Task> CaptureRequestsAsync(
+ int expectedCount,
+ Action trigger
+ )
+ {
+ List captured = [];
+ Lock sync = new();
+ TaskCompletionSource> completionSource =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ TelemetryHandler.TestSendAsyncOverride = async request =>
+ {
+ CapturedRequest capturedRequest = await CapturedRequest.CreateAsync(request);
+ lock (sync)
+ {
+ captured.Add(capturedRequest);
+ if (captured.Count == expectedCount)
+ {
+ completionSource.TrySetResult(captured.ToArray());
+ }
+ }
+
+ return new HttpResponseMessage(HttpStatusCode.OK);
+ };
+
+ trigger();
+ return await completionSource.Task.WaitAsync(TimeSpan.FromSeconds(5));
+ }
+
+ private static void ClearSettingsCaches()
+ {
+ ClearDictionaryField("booleanSettings");
+ ClearDictionaryField("valueSettings");
+ }
+
+ private static void ClearDictionaryField(string fieldName)
+ {
+ FieldInfo field = typeof(Settings).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static)!;
+ object dictionary = field.GetValue(null)!;
+ dictionary.GetType().GetMethod("Clear")!.Invoke(dictionary, null);
+ }
+
+ private sealed record CapturedRequest(
+ Uri RequestUri,
+ string Body,
+ System.Net.Http.Headers.AuthenticationHeaderValue? Authorization
+ )
+ {
+ public static async Task CreateAsync(HttpRequestMessage request) =>
+ new(
+ request.RequestUri!,
+ request.Content is null ? string.Empty : await request.Content.ReadAsStringAsync(),
+ request.Headers.Authorization
+ );
+ }
+}
diff --git a/src/UniGetUI.Interface.Telemetry.Tests/UniGetUI.Interface.Telemetry.Tests.csproj b/src/UniGetUI.Interface.Telemetry.Tests/UniGetUI.Interface.Telemetry.Tests.csproj
new file mode 100644
index 0000000000..63026ec87e
--- /dev/null
+++ b/src/UniGetUI.Interface.Telemetry.Tests/UniGetUI.Interface.Telemetry.Tests.csproj
@@ -0,0 +1,41 @@
+
+
+ $(PortableTargetFramework)
+
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI.Interface.Telemetry/InternalsVisibleTo.cs b/src/UniGetUI.Interface.Telemetry/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..569ff00f15
--- /dev/null
+++ b/src/UniGetUI.Interface.Telemetry/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.Interface.Telemetry.Tests")]
diff --git a/src/UniGetUI.Interface.Telemetry/TelemetryHandler.cs b/src/UniGetUI.Interface.Telemetry/TelemetryHandler.cs
index a63caa4ab5..0c02104c99 100644
--- a/src/UniGetUI.Interface.Telemetry/TelemetryHandler.cs
+++ b/src/UniGetUI.Interface.Telemetry/TelemetryHandler.cs
@@ -36,6 +36,7 @@ public static class TelemetryHandler
private static string _openSearchUsername = "";
private static string _openSearchPassword = "";
private static bool _credentialsWarningLogged;
+ internal static Func>? TestSendAsyncOverride;
public static void Configure(string username, string password)
{
@@ -119,45 +120,13 @@ public static async Task InitializeAsync()
.Select(m => m.Name)
.ToArray();
- int settingsMagicValue = 0;
- int mask = 0x1;
- foreach (var setting in SettingsToSend)
- {
- bool enabled = Settings.Get(
- key: setting,
- invert: Settings.ResolveKey(setting).StartsWith("Disable"));
-
- if (enabled)
- settingsMagicValue |= mask;
- mask <<= 1;
-
- if (mask == 0x1)
- throw new OverflowException();
- }
- foreach (var sp in new[] { "SP1", "SP2" })
- {
- bool enabled = sp switch
- {
- "SP1" => File.Exists("ForceUniGetUIPortable"),
- "SP2" => CoreData.WasDaemon,
- _ => throw new NotImplementedException(),
- };
-
- if (enabled)
- settingsMagicValue |= mask;
- mask <<= 1;
-
- if (mask == 0x1)
- throw new OverflowException();
- }
-
var ev = new UniGetUIActivityEvent
{
InstallID = GetRandomizedId(),
Locale = LanguageEngine.SelectedLocale,
EnabledManagers = enabledManagers,
FoundManagers = foundManagers,
- ActiveSettings = settingsMagicValue,
+ ActiveSettings = ComputeActiveSettingsBitmask(),
Application = BuildApplicationInfo(),
Platform = BuildPlatformInfo(),
};
@@ -171,6 +140,51 @@ public static async Task InitializeAsync()
}
}
+ internal static int ComputeActiveSettingsBitmask()
+ {
+ int settingsMagicValue = 0;
+ int mask = 0x1;
+ foreach (var setting in SettingsToSend)
+ {
+ bool enabled = Settings.Get(
+ key: setting,
+ invert: Settings.ResolveKey(setting).StartsWith("Disable"));
+
+ if (enabled)
+ settingsMagicValue |= mask;
+ mask <<= 1;
+
+ if (mask == 0x1)
+ throw new OverflowException();
+ }
+ foreach (var sp in new[] { "SP1", "SP2" })
+ {
+ bool enabled = sp switch
+ {
+ "SP1" => File.Exists("ForceUniGetUIPortable"),
+ "SP2" => CoreData.WasDaemon,
+ _ => throw new NotImplementedException(),
+ };
+
+ if (enabled)
+ settingsMagicValue |= mask;
+ mask <<= 1;
+
+ if (mask == 0x1)
+ throw new OverflowException();
+ }
+
+ return settingsMagicValue;
+ }
+
+ internal static void ResetTestState()
+ {
+ _openSearchUsername = "";
+ _openSearchPassword = "";
+ _credentialsWarningLogged = false;
+ TestSendAsyncOverride = null;
+ }
+
// -------------------------------------------------------------------------
public static void InstallPackage(
@@ -296,7 +310,9 @@ private static async Task PostToOpenSearchAsync(string indexName, T eventData
};
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
- HttpResponseMessage response = await _httpClient.SendAsync(request);
+ HttpResponseMessage response = TestSendAsyncOverride is { } sendAsync
+ ? await sendAsync(request)
+ : await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
Logger.Debug($"[Telemetry] Sent to {fullIndex}");
diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs
index 1be6c7e88d..95504dc411 100644
--- a/src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs
@@ -63,10 +63,11 @@ internal sealed class CargoManifestPublisher
internal sealed class CratesIOClient
{
public const string ApiUrl = "https://crates.io/api/v1";
+ internal static string? TEST_ApiUrlOverride { private get; set; }
public static Tuple GetManifest(string packageId)
{
- var manifestUrl = new Uri($"{ApiUrl}/crates/{packageId}");
+ var manifestUrl = new Uri($"{GetApiUrl()}/crates/{packageId}");
var manifest = Fetch(manifestUrl);
if (manifest.crate is null)
{
@@ -77,7 +78,7 @@ public static Tuple GetManifest(string packageId)
public static CargoManifestVersion GetManifestVersion(string packageId, string version)
{
- var manifestUrl = new Uri($"{ApiUrl}/crates/{packageId}/{version}");
+ var manifestUrl = new Uri($"{GetApiUrl()}/crates/{packageId}/{version}");
var manifest = Fetch(manifestUrl);
if (manifest.version is null)
{
@@ -86,7 +87,9 @@ public static CargoManifestVersion GetManifestVersion(string packageId, string v
return manifest.version;
}
- private static T Fetch(Uri url)
+ private static string GetApiUrl() => TEST_ApiUrlOverride ?? ApiUrl;
+
+ internal static T Fetch(Uri url)
{
HttpClient client = new(CoreTools.GenericHttpClientParameters);
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString);
diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Cargo/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Managers.Chocolatey/Chocolatey.cs b/src/UniGetUI.PackageEngine.Managers.Chocolatey/Chocolatey.cs
index 1f0445a0e7..76331f3300 100644
--- a/src/UniGetUI.PackageEngine.Managers.Chocolatey/Chocolatey.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Chocolatey/Chocolatey.cs
@@ -145,6 +145,94 @@ public static string GetProxyArgument()
+ $" --proxy-password={Uri.EscapeDataString(creds.Password)}";
}
+ internal IReadOnlyList ParseAvailableUpdates(IEnumerable lines)
+ {
+ List packages = [];
+ foreach (string line in lines)
+ {
+ if (line.StartsWith("Chocolatey"))
+ {
+ continue;
+ }
+
+ string[] elements = line.Split('|');
+ for (int i = 0; i < elements.Length; i++)
+ {
+ elements[i] = elements[i].Trim();
+ }
+
+ if (elements.Length <= 2)
+ {
+ continue;
+ }
+
+ if (
+ FALSE_PACKAGE_IDS.Contains(elements[0])
+ || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
+ || elements[1] == elements[2]
+ )
+ {
+ continue;
+ }
+
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(elements[0]),
+ elements[0],
+ elements[1],
+ elements[2],
+ DefaultSource,
+ this
+ )
+ );
+ }
+
+ return packages;
+ }
+
+ internal IReadOnlyList ParseInstalledPackages(IEnumerable lines)
+ {
+ List packages = [];
+ foreach (string line in lines)
+ {
+ if (line.StartsWith("Chocolatey"))
+ {
+ continue;
+ }
+
+ string[] elements = line.Split(' ');
+ for (int i = 0; i < elements.Length; i++)
+ {
+ elements[i] = elements[i].Trim();
+ }
+
+ if (elements.Length <= 1)
+ {
+ continue;
+ }
+
+ if (
+ FALSE_PACKAGE_IDS.Contains(elements[0])
+ || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
+ )
+ {
+ continue;
+ }
+
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(elements[0]),
+ elements[0],
+ elements[1],
+ DefaultSource,
+ this
+ )
+ );
+ }
+
+ return packages;
+ }
+
protected override IReadOnlyList GetAvailableUpdates_UnSafe()
{
using Process p = new()
@@ -166,50 +254,18 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe()
p.Start();
string? line;
- List Packages = [];
+ List lines = [];
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (!line.StartsWith("Chocolatey"))
- {
- string[] elements = line.Split('|');
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
-
- if (elements.Length <= 2)
- {
- continue;
- }
-
- if (
- FALSE_PACKAGE_IDS.Contains(elements[0])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
- || elements[1] == elements[2]
- )
- {
- continue;
- }
-
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(elements[0]),
- elements[0],
- elements[1],
- elements[2],
- DefaultSource,
- this
- )
- );
- }
+ lines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
- return Packages;
+ return ParseAvailableUpdates(lines);
}
protected override IReadOnlyList _getInstalledPackages_UnSafe()
@@ -236,48 +292,18 @@ protected override IReadOnlyList _getInstalledPackages_UnSafe()
p.Start();
string? line;
- List Packages = [];
+ List lines = [];
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (!line.StartsWith("Chocolatey"))
- {
- string[] elements = line.Split(' ');
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
-
- if (elements.Length <= 1)
- {
- continue;
- }
-
- if (
- FALSE_PACKAGE_IDS.Contains(elements[0])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
- )
- {
- continue;
- }
-
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(elements[0]),
- elements[0],
- elements[1],
- DefaultSource,
- this
- )
- );
- }
+ lines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
- return Packages;
+ return ParseInstalledPackages(lines);
}
public override IReadOnlyList FindCandidateExecutableFiles()
diff --git a/src/UniGetUI.PackageEngine.Managers.Chocolatey/Helpers/ChocolateyDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Chocolatey/Helpers/ChocolateyDetailsHelper.cs
index bc0cc244d4..1b502f6971 100644
--- a/src/UniGetUI.PackageEngine.Managers.Chocolatey/Helpers/ChocolateyDetailsHelper.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Chocolatey/Helpers/ChocolateyDetailsHelper.cs
@@ -12,6 +12,20 @@ public class ChocolateyDetailsHelper : BaseNuGetDetailsHelper
public ChocolateyDetailsHelper(BaseNuGet manager)
: base(manager) { }
+ internal static IReadOnlyList ParseInstallableVersions(IEnumerable lines)
+ {
+ List versions = [];
+ foreach (string line in lines)
+ {
+ if (line.Contains("[Approved]"))
+ {
+ versions.Add(line.Split(' ')[1].Trim());
+ }
+ }
+
+ return versions;
+ }
+
protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package)
{
using Process p = new()
@@ -40,20 +54,17 @@ protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage
p.Start();
string? line;
- List versions = [];
+ List lines = [];
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (line.Contains("[Approved]"))
- {
- versions.Add(line.Split(' ')[1].Trim());
- }
+ lines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
- return versions;
+ return ParseInstallableVersions(lines);
}
protected override string? GetInstallLocation_UnSafe(IPackage package)
diff --git a/src/UniGetUI.PackageEngine.Managers.Chocolatey/Helpers/ChocolateySourceHelper.cs b/src/UniGetUI.PackageEngine.Managers.Chocolatey/Helpers/ChocolateySourceHelper.cs
index e853958236..b8d951c648 100644
--- a/src/UniGetUI.PackageEngine.Managers.Chocolatey/Helpers/ChocolateySourceHelper.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Chocolatey/Helpers/ChocolateySourceHelper.cs
@@ -51,8 +51,6 @@ string[] Output
protected override IReadOnlyList GetSources_UnSafe()
{
- List sources = [];
-
using Process p = new()
{
StartInfo = new()
@@ -78,9 +76,26 @@ protected override IReadOnlyList GetSources_UnSafe()
p.Start();
string? line;
+ List lines = [];
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
+ lines.Add(line);
+ }
+
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+
+ return ParseSources(lines);
+ }
+
+ internal IReadOnlyList ParseSources(IEnumerable lines)
+ {
+ List sources = [];
+
+ foreach (string line in lines)
+ {
try
{
if (string.IsNullOrEmpty(line))
@@ -116,16 +131,12 @@ protected override IReadOnlyList GetSources_UnSafe()
}
}
}
- catch (Exception e)
+ catch
{
- logger.AddToStdErr(e.ToString());
+ continue;
}
}
- logger.AddToStdErr(p.StandardError.ReadToEnd());
- p.WaitForExit();
- logger.Close(p.ExitCode);
-
return sources;
}
}
diff --git a/src/UniGetUI.PackageEngine.Managers.Chocolatey/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.Chocolatey/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Chocolatey/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Managers.Dotnet/DotNet.cs b/src/UniGetUI.PackageEngine.Managers.Dotnet/DotNet.cs
index a7d7f4db23..f8ad3a5c4a 100644
--- a/src/UniGetUI.PackageEngine.Managers.Dotnet/DotNet.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Dotnet/DotNet.cs
@@ -5,6 +5,7 @@
using UniGetUI.PackageEngine.Classes.Manager;
using UniGetUI.PackageEngine.Classes.Manager.ManagerHelpers;
using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
using UniGetUI.PackageEngine.ManagerClasses.Classes;
using UniGetUI.PackageEngine.ManagerClasses.Manager;
using UniGetUI.PackageEngine.Managers.Chocolatey;
@@ -105,54 +106,18 @@ protected override IReadOnlyList _getInstalledPackages_UnSafe()
);
p.Start();
+ List outputLines = [];
string? line;
- bool DashesPassed = false;
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (!DashesPassed)
- {
- if (line.Contains("----"))
- {
- DashesPassed = true;
- }
- }
- else
- {
- string[] elements = Regex.Replace(line, " {2,}", " ").Split(' ');
- if (elements.Length < 2)
- {
- continue;
- }
-
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
-
- if (
- FALSE_PACKAGE_IDS.Contains(elements[0])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
- )
- {
- continue;
- }
-
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(elements[0]),
- elements[0],
- elements[1],
- DefaultSource,
- this,
- options
- )
- );
- }
+ outputLines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
+
+ Packages.AddRange(ParseInstalledPackages(outputLines, DefaultSource, this, options));
}
return Packages;
}
@@ -211,5 +176,61 @@ protected override void _loadManagerVersion(out string version)
process.Start();
version = process.StandardOutput.ReadToEnd().Trim();
}
+
+ internal static IReadOnlyList ParseInstalledPackages(
+ IEnumerable outputLines,
+ IManagerSource source,
+ IPackageManager manager,
+ OverridenInstallationOptions options
+ )
+ {
+ List packages = [];
+ bool dashesPassed = false;
+
+ foreach (string rawLine in outputLines)
+ {
+ if (!dashesPassed)
+ {
+ if (rawLine.Contains("----"))
+ {
+ dashesPassed = true;
+ }
+
+ continue;
+ }
+
+ string[] elements = Regex.Replace(rawLine, " {2,}", " ").Split(' ');
+ if (elements.Length < 2)
+ {
+ continue;
+ }
+
+ for (int i = 0; i < elements.Length; i++)
+ {
+ elements[i] = elements[i].Trim();
+ }
+
+ if (
+ FALSE_PACKAGE_IDS.Contains(elements[0])
+ || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
+ )
+ {
+ continue;
+ }
+
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(elements[0]),
+ elements[0],
+ elements[1],
+ source,
+ manager,
+ options
+ )
+ );
+ }
+
+ return packages;
+ }
}
}
diff --git a/src/UniGetUI.PackageEngine.Managers.Dotnet/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.Dotnet/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Dotnet/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Managers.Npm/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.Npm/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Npm/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Managers.Npm/Npm.cs b/src/UniGetUI.PackageEngine.Managers.Npm/Npm.cs
index 26356531a7..6532237b35 100644
--- a/src/UniGetUI.PackageEngine.Managers.Npm/Npm.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Npm/Npm.cs
@@ -7,6 +7,7 @@
using UniGetUI.PackageEngine.Classes.Manager;
using UniGetUI.PackageEngine.Classes.Manager.ManagerHelpers;
using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
using UniGetUI.PackageEngine.ManagerClasses.Classes;
using UniGetUI.PackageEngine.ManagerClasses.Manager;
using UniGetUI.PackageEngine.PackageClasses;
@@ -79,49 +80,7 @@ protected override IReadOnlyList FindPackages_UnSafe(string query)
p.WaitForExit();
logger.Close(p.ExitCode);
- List Packages = [];
-
- void TryAdd(JsonNode? node)
- {
- string? id = node?["name"]?.ToString();
- string? version = node?["version"]?.ToString();
- if (id is not null && version is not null)
- Packages.Add(new Package(CoreTools.FormatAsName(id), id, version, DefaultSource, this));
- }
-
- // npm may emit warning lines before the JSON payload (even with --json).
- // npm v7+ outputs a JSON array; npm v6 and earlier outputs NDJSON (one object per line).
- // Try JSON array first (find the first '['); fall back to NDJSON if parsing fails.
- bool parsedAsArray = false;
- int arrayStart = strContents.IndexOf('[');
- if (arrayStart >= 0)
- {
- try
- {
- JsonArray? results = JsonNode.Parse(strContents[arrayStart..]) as JsonArray;
- foreach (JsonNode? entry in results ?? [])
- TryAdd(entry);
- parsedAsArray = true;
- }
- catch (Exception e)
- {
- Logger.Warn($"npm search JSON array parse failed, falling back to NDJSON: {e.Message}");
- }
- }
-
- if (!parsedAsArray)
- {
- // NDJSON fallback (npm v6): one complete JSON object per line
- foreach (string line in strContents.Split('\n'))
- {
- string trimmed = line.Trim();
- if (!trimmed.StartsWith("{")) continue;
- try { TryAdd(JsonNode.Parse(trimmed)); }
- catch (Exception e) { Logger.Warn($"npm search NDJSON line parse failed: {e.Message}"); }
- }
- }
-
- return Packages;
+ return ParseSearchOutput(strContents, DefaultSource, this);
}
protected override IReadOnlyList GetAvailableUpdates_UnSafe()
@@ -161,28 +120,7 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe()
string strContents = p.StandardOutput.ReadToEnd();
logger.AddToStdOut(strContents);
- JsonObject? contents = null;
- if (strContents.Any())
- contents = JsonNode.Parse(strContents) as JsonObject;
- foreach (var (packageId, packageData) in contents?.ToDictionary() ?? [])
- {
- string? version = packageData?["current"]?.ToString();
- string? newVersion = packageData?["latest"]?.ToString();
- if (version is not null && newVersion is not null)
- {
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(packageId),
- packageId,
- version,
- newVersion,
- DefaultSource,
- this,
- options
- )
- );
- }
- }
+ Packages.AddRange(ParseAvailableUpdatesOutput(strContents, DefaultSource, this, options));
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
@@ -231,27 +169,7 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe()
string strContents = p.StandardOutput.ReadToEnd();
logger.AddToStdOut(strContents);
- JsonObject? contents = null;
- if (strContents.Any())
- contents =
- (JsonNode.Parse(strContents) as JsonObject)?["dependencies"] as JsonObject;
- foreach (var (packageId, packageData) in contents?.ToDictionary() ?? [])
- {
- string? version = packageData?["version"]?.ToString();
- if (version is not null)
- {
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(packageId),
- packageId,
- version,
- DefaultSource,
- this,
- options
- )
- );
- }
- }
+ Packages.AddRange(ParseInstalledPackagesOutput(strContents, DefaultSource, this, options));
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
@@ -309,5 +227,128 @@ protected override void _loadManagerVersion(out string version)
version = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
}
+
+ internal static IReadOnlyList ParseSearchOutput(
+ string output,
+ IManagerSource source,
+ IPackageManager manager
+ )
+ {
+ List packages = [];
+
+ void TryAdd(JsonNode? node)
+ {
+ string? id = node?["name"]?.ToString();
+ string? version = node?["version"]?.ToString();
+ if (id is not null && version is not null)
+ packages.Add(new Package(CoreTools.FormatAsName(id), id, version, source, manager));
+ }
+
+ bool parsedAsArray = false;
+ int arrayStart = output.IndexOf('[');
+ if (arrayStart >= 0)
+ {
+ try
+ {
+ JsonArray? results = JsonNode.Parse(output[arrayStart..]) as JsonArray;
+ foreach (JsonNode? entry in results ?? [])
+ TryAdd(entry);
+ parsedAsArray = true;
+ }
+ catch (Exception e)
+ {
+ Logger.Warn($"npm search JSON array parse failed, falling back to NDJSON: {e.Message}");
+ }
+ }
+
+ if (!parsedAsArray)
+ {
+ foreach (string line in output.Split('\n'))
+ {
+ string trimmed = line.Trim();
+ if (!trimmed.StartsWith("{"))
+ continue;
+
+ try
+ {
+ TryAdd(JsonNode.Parse(trimmed));
+ }
+ catch (Exception e)
+ {
+ Logger.Warn($"npm search NDJSON line parse failed: {e.Message}");
+ }
+ }
+ }
+
+ return packages;
+ }
+
+ internal static IReadOnlyList ParseAvailableUpdatesOutput(
+ string output,
+ IManagerSource source,
+ IPackageManager manager,
+ OverridenInstallationOptions options
+ )
+ {
+ List packages = [];
+ if (!output.Any())
+ return packages;
+
+ JsonObject? contents = JsonNode.Parse(output) as JsonObject;
+ foreach (var (packageId, packageData) in contents?.ToDictionary() ?? [])
+ {
+ string? version = packageData?["current"]?.ToString();
+ string? newVersion = packageData?["latest"]?.ToString();
+ if (version is not null && newVersion is not null)
+ {
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(packageId),
+ packageId,
+ version,
+ newVersion,
+ source,
+ manager,
+ options
+ )
+ );
+ }
+ }
+
+ return packages;
+ }
+
+ internal static IReadOnlyList ParseInstalledPackagesOutput(
+ string output,
+ IManagerSource source,
+ IPackageManager manager,
+ OverridenInstallationOptions options
+ )
+ {
+ List packages = [];
+ if (!output.Any())
+ return packages;
+
+ JsonObject? contents = (JsonNode.Parse(output) as JsonObject)?["dependencies"] as JsonObject;
+ foreach (var (packageId, packageData) in contents?.ToDictionary() ?? [])
+ {
+ string? version = packageData?["version"]?.ToString();
+ if (version is not null)
+ {
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(packageId),
+ packageId,
+ version,
+ source,
+ manager,
+ options
+ )
+ );
+ }
+ }
+
+ return packages;
+ }
}
}
diff --git a/src/UniGetUI.PackageEngine.Managers.Pip/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.Pip/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Pip/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs
index 785afd004d..a0cb271eda 100644
--- a/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs
@@ -8,6 +8,7 @@
using UniGetUI.Interface.Enums;
using UniGetUI.PackageEngine.Classes.Manager;
using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
using UniGetUI.PackageEngine.ManagerClasses.Classes;
using UniGetUI.PackageEngine.ManagerClasses.Manager;
using UniGetUI.PackageEngine.PackageClasses;
@@ -128,13 +129,7 @@ protected override IReadOnlyList FindPackages_UnSafe(string query)
{
string[] allNames = GetOrRefreshIndex(logger);
- string queryLower = query.ToLowerInvariant();
- string[] matches = allNames
- .Where(n => n.Contains(queryLower, StringComparison.OrdinalIgnoreCase))
- .OrderBy(n => n.StartsWith(queryLower, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
- .ThenBy(n => n.Length)
- .Take(MaxSearchResults)
- .ToArray();
+ string[] matches = SelectSearchMatches(query, allNames);
logger.Log($"Matched {matches.Length} packages for query '{query}'");
@@ -203,16 +198,7 @@ private static string[] GetOrRefreshIndex(INativeTaskLogger logger)
throw new HttpRequestException($"PyPI simple index returned {(int)response.StatusCode} {response.ReasonPhrase}");
string json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
-
- var projects = (JsonNode.Parse(json) as JsonObject)?["projects"] as JsonArray;
- string[] names = projects?
- .Select(p => p?["name"]?.GetValue())
- .Where(n => !string.IsNullOrEmpty(n))
- .Select(n => n!)
- .ToArray() ?? [];
-
- if (names.Length == 0)
- throw new InvalidDataException("PyPI simple index returned 0 packages — response may be malformed");
+ string[] names = ParseSimpleIndexProjectNames(json);
logger.Log($"Downloaded {names.Length} package names from PyPI");
@@ -232,6 +218,32 @@ private static string[] GetOrRefreshIndex(INativeTaskLogger logger)
return names;
}
+ internal static string[] ParseSimpleIndexProjectNames(string json)
+ {
+ var projects = (JsonNode.Parse(json) as JsonObject)?["projects"] as JsonArray;
+ string[] names = projects?
+ .Select(p => p?["name"]?.GetValue())
+ .Where(n => !string.IsNullOrEmpty(n))
+ .Select(n => n!)
+ .ToArray() ?? [];
+
+ if (names.Length == 0)
+ throw new InvalidDataException("PyPI simple index returned 0 packages — response may be malformed");
+
+ return names;
+ }
+
+ internal static string[] SelectSearchMatches(string query, IEnumerable allNames)
+ {
+ string queryLower = query.ToLowerInvariant();
+ return allNames
+ .Where(n => n.Contains(queryLower, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(n => n.StartsWith(queryLower, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
+ .ThenBy(n => n.Length)
+ .Take(MaxSearchResults)
+ .ToArray();
+ }
+
private static async Task FetchLatestVersionAsync(string packageName)
{
await _versionFetchSemaphore.WaitAsync().ConfigureAwait(false);
@@ -274,58 +286,18 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe()
p.Start();
string? line;
- bool DashesPassed = false;
- List Packages = [];
+ List outputLines = [];
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (!DashesPassed)
- {
- if (line.Contains("----"))
- {
- DashesPassed = true;
- }
- }
- else
- {
- string[] elements = Regex.Replace(line, " {2,}", " ").Split(' ');
- if (elements.Length < 3)
- {
- continue;
- }
-
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
-
- if (
- FALSE_PACKAGE_IDS.Contains(elements[0])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
- )
- {
- continue;
- }
-
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(elements[0]),
- elements[0],
- elements[1],
- elements[2],
- DefaultSource,
- this,
- new(PackageScope.Global)
- )
- );
- }
+ outputLines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
- return Packages;
+ return ParseAvailableUpdates(outputLines, DefaultSource, this);
}
protected override IReadOnlyList GetInstalledPackages_UnSafe()
@@ -352,57 +324,99 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe()
p.Start();
string? line;
- bool DashesPassed = false;
- List Packages = [];
+ List outputLines = [];
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (!DashesPassed)
+ outputLines.Add(line);
+ }
+
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+
+ return ParseInstalledPackages(outputLines, DefaultSource, this);
+ }
+
+ internal static IReadOnlyList ParseAvailableUpdates(
+ IEnumerable outputLines,
+ IManagerSource source,
+ Pip manager
+ )
+ {
+ return ParsePackages(outputLines, source, manager, expectAvailableVersion: true);
+ }
+
+ internal static IReadOnlyList ParseInstalledPackages(
+ IEnumerable outputLines,
+ IManagerSource source,
+ Pip manager
+ )
+ {
+ return ParsePackages(outputLines, source, manager, expectAvailableVersion: false);
+ }
+
+ private static IReadOnlyList ParsePackages(
+ IEnumerable outputLines,
+ IManagerSource source,
+ Pip manager,
+ bool expectAvailableVersion
+ )
+ {
+ bool dashesPassed = false;
+ List packages = [];
+ int requiredElements = expectAvailableVersion ? 3 : 2;
+
+ foreach (string line in outputLines)
+ {
+ if (!dashesPassed)
{
if (line.Contains("----"))
{
- DashesPassed = true;
+ dashesPassed = true;
}
+ continue;
}
- else
- {
- string[] elements = Regex.Replace(line, " {2,}", " ").Split(' ');
- if (elements.Length < 2)
- {
- continue;
- }
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
+ string[] elements = Regex
+ .Replace(line.Trim(), " {2,}", " ")
+ .Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (elements.Length < requiredElements)
+ {
+ continue;
+ }
- if (
- FALSE_PACKAGE_IDS.Contains(elements[0])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
- )
- {
- continue;
- }
+ if (
+ FALSE_PACKAGE_IDS.Contains(elements[0])
+ || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
+ )
+ {
+ continue;
+ }
- Packages.Add(
- new Package(
+ packages.Add(
+ expectAvailableVersion
+ ? new Package(
CoreTools.FormatAsName(elements[0]),
elements[0],
elements[1],
- DefaultSource,
- this,
+ elements[2],
+ source,
+ manager,
new(PackageScope.Global)
)
- );
- }
+ : new Package(
+ CoreTools.FormatAsName(elements[0]),
+ elements[0],
+ elements[1],
+ source,
+ manager,
+ new(PackageScope.Global)
+ )
+ );
}
- logger.AddToStdErr(p.StandardError.ReadToEnd());
- p.WaitForExit();
- logger.Close(p.ExitCode);
-
- return Packages;
+ return packages;
}
public override IReadOnlyList FindCandidateExecutableFiles()
diff --git a/src/UniGetUI.PackageEngine.Managers.PowerShell/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.PowerShell/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.PowerShell/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Managers.PowerShell/PowerShell.cs b/src/UniGetUI.PackageEngine.Managers.PowerShell/PowerShell.cs
index c1d3ca7f28..e2b4d30ce2 100644
--- a/src/UniGetUI.PackageEngine.Managers.PowerShell/PowerShell.cs
+++ b/src/UniGetUI.PackageEngine.Managers.PowerShell/PowerShell.cs
@@ -100,48 +100,18 @@ protected override IReadOnlyList _getInstalledPackages_UnSafe()
p.Start();
string? line;
- List Packages = [];
- bool DashesPassed = false;
+ List outputLines = [];
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (!DashesPassed)
- {
- if (line.Contains("-----"))
- {
- DashesPassed = true;
- }
- }
- else
- {
- string[] elements = Regex.Replace(line, " {2,}", " ").Split(' ');
- if (elements.Length < 3)
- {
- continue;
- }
-
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
-
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(elements[1]),
- elements[1],
- elements[0],
- SourcesHelper.Factory.GetSourceOrDefault(elements[2]),
- this
- )
- );
- }
+ outputLines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
- return Packages;
+ return ParseInstalledPackages(outputLines, this);
}
public override List FindCandidateExecutableFiles()
@@ -182,5 +152,50 @@ protected override void _loadManagerVersion(out string version)
process.Start();
version = process.StandardOutput.ReadToEnd().Trim();
}
+
+ internal static IReadOnlyList ParseInstalledPackages(
+ IEnumerable outputLines,
+ PowerShell manager
+ )
+ {
+ List packages = [];
+ bool dashesPassed = false;
+
+ foreach (string rawLine in outputLines)
+ {
+ if (!dashesPassed)
+ {
+ if (rawLine.Contains("-----"))
+ {
+ dashesPassed = true;
+ }
+
+ continue;
+ }
+
+ string[] elements = Regex.Replace(rawLine, " {2,}", " ").Split(' ');
+ if (elements.Length < 3)
+ {
+ continue;
+ }
+
+ for (int i = 0; i < elements.Length; i++)
+ {
+ elements[i] = elements[i].Trim();
+ }
+
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(elements[1]),
+ elements[1],
+ elements[0],
+ manager.SourcesHelper.Factory.GetSourceOrDefault(elements[2]),
+ manager
+ )
+ );
+ }
+
+ return packages;
+ }
}
}
diff --git a/src/UniGetUI.PackageEngine.Managers.Scoop/Helpers/ScoopSourceHelper.cs b/src/UniGetUI.PackageEngine.Managers.Scoop/Helpers/ScoopSourceHelper.cs
index 11dfa45281..44b55e9a36 100644
--- a/src/UniGetUI.PackageEngine.Managers.Scoop/Helpers/ScoopSourceHelper.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Scoop/Helpers/ScoopSourceHelper.cs
@@ -64,81 +64,100 @@ protected override IReadOnlyList GetSources_UnSafe()
LoggableTaskType.ListSources,
p
);
- List sources = [];
p.Start();
- bool DashesPassed = false;
-
+ List lines = [];
string? line;
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
+ lines.Add(line);
+ }
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+
+ return ParseSources(lines);
+ }
+
+ internal IReadOnlyList ParseSources(IEnumerable lines)
+ {
+ List sources = [];
+ bool dashesPassed = false;
+
+ foreach (string line in lines)
+ {
try
{
- if (!DashesPassed)
+ if (!dashesPassed)
{
if (line.Contains("---"))
{
- DashesPassed = true;
+ dashesPassed = true;
}
+
+ continue;
}
- else if (line.Trim() != "")
+
+ if (string.IsNullOrWhiteSpace(line))
{
- string[] elements = Regex
- .Replace(
- Regex.Replace(line, "[1234567890 :.-][AaPp][Mm][\\W]", "").Trim(),
- " {2,}",
- " "
+ continue;
+ }
+
+ string[] elements = Regex
+ .Replace(
+ Regex.Replace(line, "[1234567890 :.-][AaPp][Mm][\\W]", "").Trim(),
+ " {2,}",
+ " "
+ )
+ .Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (elements.Length < 5)
+ {
+ continue;
+ }
+
+ if (
+ !elements[1].Contains("https://")
+ && !elements[1].Contains("http://")
+ )
+ {
+ elements[1] = Path.Join(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ "scoop",
+ "buckets",
+ elements[0].Trim()
+ );
+ }
+ else
+ {
+ elements[1] = Regex.Replace(elements[1], @"^(.*)\.git$", "$1");
+ }
+
+ try
+ {
+ sources.Add(
+ new ManagerSource(
+ Manager,
+ elements[0].Trim(),
+ new Uri(elements[1]),
+ int.Parse(elements[4].Trim()),
+ elements[2].Trim() + " " + elements[3].Trim()
)
- .Split(' ');
- if (elements.Length >= 5)
- {
- if (
- !elements[1].Contains("https://")
- && !elements[1].Contains("http://")
+ );
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn(ex);
+ sources.Add(
+ new ManagerSource(
+ Manager,
+ elements[0].Trim(),
+ new Uri(elements[1]),
+ -1,
+ "1/1/1970"
)
- {
- elements[1] = Path.Join(
- Environment.GetFolderPath(
- Environment.SpecialFolder.UserProfile
- ),
- "scoop",
- "buckets",
- elements[0].Trim()
- );
- }
- else
- {
- elements[1] = Regex.Replace(elements[1], @"^(.*)\.git$", "$1");
- }
-
- try
- {
- sources.Add(
- new ManagerSource(
- Manager,
- elements[0].Trim(),
- new Uri(elements[1]),
- int.Parse(elements[4].Trim()),
- elements[2].Trim() + " " + elements[3].Trim()
- )
- );
- }
- catch (Exception ex)
- {
- logger.AddToStdErr(ex.ToString());
- sources.Add(
- new ManagerSource(
- Manager,
- elements[0].Trim(),
- new Uri(elements[1]),
- -1,
- "1/1/1970"
- )
- );
- }
- }
+ );
}
}
catch (Exception e)
@@ -146,9 +165,6 @@ protected override IReadOnlyList GetSources_UnSafe()
Logger.Warn(e);
}
}
- logger.AddToStdErr(p.StandardError.ReadToEnd());
- p.WaitForExit();
- logger.Close(p.ExitCode);
return sources;
}
diff --git a/src/UniGetUI.PackageEngine.Managers.Scoop/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.Scoop/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Scoop/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs b/src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs
index 736a728085..df2bd2e591 100644
--- a/src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs
+++ b/src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs
@@ -159,10 +159,216 @@ public Scoop()
OperationHelper = new ScoopPkgOperationHelper(this);
}
- protected override IReadOnlyList FindPackages_UnSafe(string query)
+ internal IReadOnlyList ParseSearchOutput(IEnumerable lines)
{
- List Packages = [];
+ List packages = [];
+ IManagerSource source = Properties.DefaultSource;
+
+ foreach (string line in lines)
+ {
+ if (line.StartsWith("'"))
+ {
+ string sourceName = line.Split(" ")[0].Replace("'", "");
+ source = SourcesHelper.Factory.GetSourceOrDefault(sourceName);
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ string[] elements = line.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (elements.Length < 2)
+ {
+ continue;
+ }
+ for (int i = 0; i < elements.Length; i++)
+ {
+ elements[i] = elements[i].Trim();
+ }
+
+ if (
+ FALSE_PACKAGE_IDS.Contains(elements[0])
+ || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
+ )
+ {
+ continue;
+ }
+
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(elements[0]),
+ elements[0],
+ elements[1].Replace("(", "").Replace(")", ""),
+ source,
+ this
+ )
+ );
+ }
+
+ return packages;
+ }
+
+ internal IReadOnlyList ParseAvailableUpdates(
+ IEnumerable lines,
+ IEnumerable installedPackages
+ )
+ {
+ Dictionary installedPackageMap = [];
+ foreach (IPackage installedPackage in installedPackages)
+ {
+ string key = installedPackage.Id + "." + installedPackage.VersionString;
+ if (!installedPackageMap.ContainsKey(key))
+ {
+ installedPackageMap.Add(key, installedPackage);
+ }
+ }
+
+ List packages = [];
+ bool dashesPassed = false;
+ foreach (string line in lines)
+ {
+ if (!dashesPassed)
+ {
+ if (line.Contains("---"))
+ {
+ dashesPassed = true;
+ }
+
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ string[] elements = Regex
+ .Replace(line, " {2,}", " ")
+ .Trim()
+ .Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (elements.Length < 3)
+ {
+ continue;
+ }
+
+ for (int i = 0; i < elements.Length; i++)
+ {
+ elements[i] = elements[i].Trim();
+ }
+
+ if (
+ FALSE_PACKAGE_IDS.Contains(elements[0])
+ || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
+ || FALSE_PACKAGE_VERSIONS.Contains(elements[2])
+ )
+ {
+ continue;
+ }
+
+ if (
+ installedPackageMap.TryGetValue(
+ elements[0] + "." + elements[1],
+ out IPackage? installedPackage
+ )
+ )
+ {
+ OverridenInstallationOptions options = new(installedPackage.OverridenOptions.Scope);
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(elements[0]),
+ elements[0],
+ elements[1],
+ elements[2],
+ installedPackage.Source,
+ this,
+ options
+ )
+ );
+ }
+ }
+
+ return packages;
+ }
+
+ internal IReadOnlyList ParseInstalledPackages(IEnumerable lines)
+ {
+ List packages = [];
+ bool dashesPassed = false;
+ foreach (string line in lines)
+ {
+ if (!dashesPassed)
+ {
+ if (line.Contains("---"))
+ {
+ dashesPassed = true;
+ }
+
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ string[] elements = Regex
+ .Replace(line, " {2,}", " ")
+ .Trim()
+ .Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (elements.Length < 3)
+ {
+ continue;
+ }
+
+ if (elements[2].Contains(":\\"))
+ {
+ var path = Regex.Match(
+ line,
+ "[A-Za-z]:(?:[\\\\\\/][^\\\\\\/\\n]+)+(?:.json|…)"
+ );
+ if (!string.IsNullOrEmpty(path.Value))
+ {
+ elements[2] = path.Value;
+ }
+ }
+
+ for (int i = 0; i < elements.Length; i++)
+ {
+ elements[i] = elements[i].Trim();
+ }
+
+ if (
+ FALSE_PACKAGE_IDS.Contains(elements[0])
+ || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
+ )
+ {
+ continue;
+ }
+
+ OverridenInstallationOptions options = new(
+ line.Contains("Global install") ? PackageScope.Global : PackageScope.User
+ );
+
+ packages.Add(
+ new Package(
+ CoreTools.FormatAsName(elements[0]),
+ elements[0],
+ elements[1],
+ SourcesHelper.Factory.GetSourceOrDefault(elements[2]),
+ this,
+ options
+ )
+ );
+ }
+
+ return packages;
+ }
+
+ protected override IReadOnlyList FindPackages_UnSafe(string query)
+ {
var (found, path) = CoreTools.Which("scoop-search.exe");
if (!found)
{
@@ -208,73 +414,22 @@ protected override IReadOnlyList FindPackages_UnSafe(string query)
p.Start();
+ List lines = [];
string? line;
- IManagerSource source = Properties.DefaultSource;
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (line.StartsWith("'"))
- {
- string sourceName = line.Split(" ")[0].Replace("'", "");
- source = SourcesHelper.Factory.GetSourceOrDefault(sourceName);
- }
- else if (line.Trim() != "")
- {
- string[] elements = line.Trim().Split(" ");
- if (elements.Length < 2)
- {
- continue;
- }
-
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
-
- if (
- FALSE_PACKAGE_IDS.Contains(elements[0])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
- )
- {
- continue;
- }
-
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(elements[0]),
- elements[0],
- elements[1].Replace("(", "").Replace(")", ""),
- source,
- this
- )
- );
- }
+ lines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
- return Packages;
+ return ParseSearchOutput(lines);
}
protected override IReadOnlyList GetAvailableUpdates_UnSafe()
{
- Dictionary InstalledPackages = [];
- foreach (IPackage InstalledPackage in GetInstalledPackages())
- {
- if (
- !InstalledPackages.ContainsKey(
- InstalledPackage.Id + "." + InstalledPackage.VersionString
- )
- )
- {
- InstalledPackages.Add(
- InstalledPackage.Id + "." + InstalledPackage.VersionString,
- InstalledPackage
- );
- }
- }
-
- List Packages = [];
+ IReadOnlyList installedPackages = GetInstalledPackages();
using Process p = new()
{
@@ -293,75 +448,17 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe()
p.Start();
+ List lines = [];
string? line;
- bool DashesPassed = false;
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (!DashesPassed)
- {
- if (line.Contains("---"))
- {
- DashesPassed = true;
- }
- }
- else if (line.Trim() != "")
- {
- string[] elements = Regex.Replace(line, " {2,}", " ").Trim().Split(" ");
- if (elements.Length < 3)
- {
- continue;
- }
-
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
-
- if (
- FALSE_PACKAGE_IDS.Contains(elements[0])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[2])
- )
- {
- continue;
- }
-
- if (
- InstalledPackages.TryGetValue(
- elements[0] + "." + elements[1],
- out IPackage? InstalledPackage
- )
- )
- {
- OverridenInstallationOptions options = new(
- InstalledPackage.OverridenOptions.Scope
- );
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(elements[0]),
- elements[0],
- elements[1],
- elements[2],
- InstalledPackage.Source,
- this,
- options
- )
- );
- }
- else
- {
- Logger.Warn(
- "Upgradable scoop package not listed on installed packages - id="
- + elements[0]
- );
- }
- }
+ lines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
- return Packages;
+ return ParseAvailableUpdates(lines, installedPackages);
}
protected override IReadOnlyList GetInstalledPackages_UnSafe() =>
@@ -369,8 +466,6 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe() =>
private IReadOnlyList _getInstalledPackages_UnSafe()
{
- List Packages = [];
-
using Process p = new()
{
StartInfo = new ProcessStartInfo
@@ -390,66 +485,17 @@ private IReadOnlyList _getInstalledPackages_UnSafe()
);
p.Start();
+ List lines = [];
string? line;
- bool DashesPassed = false;
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
- if (!DashesPassed)
- {
- if (line.Contains("---"))
- {
- DashesPassed = true;
- }
- }
- else if (line.Trim() != "")
- {
- string[] elements = Regex.Replace(line, " {2,}", " ").Trim().Split(" ");
- if (elements.Length < 3)
- continue;
-
- if (elements[2].Contains(":\\"))
- {
- var path = Regex.Match(
- line,
- "[A-Za-z]:(?:[\\\\\\/][^\\\\\\/\\n]+)+(?:.json|…)"
- );
- elements[2] = path.Value;
- }
-
- for (int i = 0; i < elements.Length; i++)
- {
- elements[i] = elements[i].Trim();
- }
-
- if (
- FALSE_PACKAGE_IDS.Contains(elements[0])
- || FALSE_PACKAGE_VERSIONS.Contains(elements[1])
- )
- {
- continue;
- }
-
- OverridenInstallationOptions options = new(
- line.Contains("Global install") ? PackageScope.Global : PackageScope.User
- );
-
- Packages.Add(
- new Package(
- CoreTools.FormatAsName(elements[0]),
- elements[0],
- elements[1],
- SourcesHelper.Factory.GetSourceOrDefault(elements[2]),
- this,
- options
- )
- );
- }
+ lines.Add(line);
}
logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
- return Packages;
+ return ParseInstalledPackages(lines);
}
public override void RefreshPackageIndexes()
diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..eeb63dad19
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.WinGet/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")]
diff --git a/src/UniGetUI.PackageEngine.Tests/CargoClientTests.cs b/src/UniGetUI.PackageEngine.Tests/CargoClientTests.cs
new file mode 100644
index 0000000000..57b6e883dd
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/CargoClientTests.cs
@@ -0,0 +1,100 @@
+using UniGetUI.PackageEngine.Managers.CargoManager;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class CargoClientTests : IDisposable
+{
+ public void Dispose()
+ {
+ CratesIOClient.TEST_ApiUrlOverride = null;
+ }
+
+ [Fact]
+ public void GetManifest_ReturnsManifestAndResolvedUri()
+ {
+ using var server = new TestHttpServer(request =>
+ {
+ return request.Url?.AbsolutePath switch
+ {
+ "/api/v1/crates/ripgrep" => (
+ 200,
+ """
+ {
+ "crate": {
+ "max_stable_version": "14.1.0",
+ "max_version": "14.1.0",
+ "name": "ripgrep",
+ "newest_version": "14.1.0"
+ },
+ "versions": [
+ {
+ "checksum": "abc",
+ "dl_path": "/api/v1/crates/ripgrep/14.1.0/download",
+ "num": "14.1.0",
+ "yanked": false
+ }
+ ]
+ }
+ """,
+ "application/json"
+ ),
+ _ => (404, string.Empty, "application/json"),
+ };
+ });
+ CratesIOClient.TEST_ApiUrlOverride = $"{server.BaseUri.AbsoluteUri.TrimEnd('/')}/api/v1";
+
+ var (uri, manifest) = CratesIOClient.GetManifest("ripgrep");
+
+ Assert.Equal($"{server.BaseUri.AbsoluteUri.TrimEnd('/')}/api/v1/crates/ripgrep", uri.ToString());
+ Assert.Equal("ripgrep", manifest.crate.name);
+ Assert.Equal("14.1.0", Assert.Single(manifest.versions).num);
+ }
+
+ [Fact]
+ public void GetManifestVersion_ReturnsWrappedVersion()
+ {
+ using var server = new TestHttpServer(request =>
+ {
+ return request.Url?.AbsolutePath switch
+ {
+ "/api/v1/crates/ripgrep/14.1.0" => (
+ 200,
+ """
+ {
+ "version": {
+ "checksum": "abc",
+ "dl_path": "/api/v1/crates/ripgrep/14.1.0/download",
+ "num": "14.1.0",
+ "yanked": false
+ }
+ }
+ """,
+ "application/json"
+ ),
+ _ => (404, string.Empty, "application/json"),
+ };
+ });
+ CratesIOClient.TEST_ApiUrlOverride = $"{server.BaseUri.AbsoluteUri.TrimEnd('/')}/api/v1";
+
+ var version = CratesIOClient.GetManifestVersion("ripgrep", "14.1.0");
+
+ Assert.Equal("14.1.0", version.num);
+ }
+
+ [Fact]
+ public void GetManifest_ThrowsWhenResponseContainsNullCrate()
+ {
+ using var server = new TestHttpServer(request =>
+ {
+ return request.Url?.AbsolutePath switch
+ {
+ "/api/v1/crates/ripgrep" => (200, """{"crate":null,"versions":[]}""", "application/json"),
+ _ => (404, string.Empty, "application/json"),
+ };
+ });
+ CratesIOClient.TEST_ApiUrlOverride = $"{server.BaseUri.AbsoluteUri.TrimEnd('/')}/api/v1";
+
+ Assert.Throws(() => CratesIOClient.GetManifest("ripgrep"));
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/ChocolateyManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/ChocolateyManagerTests.cs
new file mode 100644
index 0000000000..367b0b2f7a
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/ChocolateyManagerTests.cs
@@ -0,0 +1,277 @@
+#if WINDOWS
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Managers.Choco;
+using UniGetUI.PackageEngine.Managers.ChocolateyManager;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Structs;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+using Architecture = UniGetUI.PackageEngine.Enums.Architecture;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+[CollectionDefinition("Chocolatey manager tests", DisableParallelization = true)]
+public sealed class ChocolateyManagerTestCollection
+{
+ public const string Name = "Chocolatey manager tests";
+}
+
+[Collection(ChocolateyManagerTestCollection.Name)]
+public sealed class ChocolateyManagerTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ AppContext.BaseDirectory,
+ nameof(ChocolateyManagerTests),
+ Guid.NewGuid().ToString("N")
+ );
+
+ public ChocolateyManagerTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Settings.ResetSettings();
+ Settings.Set(Settings.K.EnableProxy, false);
+ Settings.Set(Settings.K.EnableProxyAuth, false);
+ Settings.Set(Settings.K.UseSystemChocolatey, false);
+ Settings.SetValue(Settings.K.ProxyURL, "");
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void GetProxyArgumentReturnsConfiguredProxyWhenAuthenticationIsDisabled()
+ {
+ Settings.Set(Settings.K.EnableProxy, true);
+ Settings.Set(Settings.K.EnableProxyAuth, false);
+ Settings.SetValue(Settings.K.ProxyURL, "http://proxy.example.test:3128/");
+
+ Assert.Equal("--proxy http://proxy.example.test:3128/", Chocolatey.GetProxyArgument());
+ }
+
+ [Fact]
+ public void ParseAvailableUpdatesFiltersNoiseAndBuildsPackagesFromFixture()
+ {
+ var manager = new Chocolatey();
+
+ var packages = manager.ParseAvailableUpdates(
+ ReadFixtureLines("Chocolatey\\outdated-output.txt")
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ Assert.Equal("git", package.Id);
+ Assert.Equal("2.47.0", package.VersionString);
+ Assert.Equal("2.48.1", package.NewVersionString);
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ },
+ package =>
+ {
+ Assert.Equal("7zip", package.Id);
+ Assert.Equal("24.9.0", package.VersionString);
+ Assert.Equal("25.0.0", package.NewVersionString);
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseInstalledPackagesFiltersNoiseAndBuildsPackagesFromFixture()
+ {
+ var manager = new Chocolatey();
+
+ var packages = manager.ParseInstalledPackages(ReadFixtureLines("Chocolatey\\list-output.txt"));
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ Assert.Equal("git", package.Id);
+ Assert.Equal("2.47.0", package.VersionString);
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ },
+ package =>
+ {
+ Assert.Equal("7zip", package.Id);
+ Assert.Equal("24.9.0", package.VersionString);
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseSourcesNormalizesCommunityFeedsAndPreservesCustomFeeds()
+ {
+ var manager = new Chocolatey();
+ var helper = Assert.IsType(manager.SourcesHelper);
+
+ var sources = helper.ParseSources(ReadFixtureLines("Chocolatey\\source-list-output.txt"));
+
+ Assert.Collection(
+ sources,
+ source =>
+ {
+ Assert.Equal("community", source.Name);
+ Assert.Equal(new Uri("https://community.chocolatey.org/api/v2/"), source.Url);
+ },
+ source =>
+ {
+ Assert.Equal("community", source.Name);
+ Assert.Equal(new Uri("https://community.chocolatey.org/api/v2/"), source.Url);
+ },
+ source =>
+ {
+ Assert.Equal("internal repo", source.Name);
+ Assert.Equal(new Uri("https://packages.example.test/api/v2/"), source.Url);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseInstallableVersionsReadsApprovedVersionsFromFixture()
+ {
+ var versions = ChocolateyDetailsHelper.ParseInstallableVersions(
+ ReadFixtureLines("Chocolatey\\search-versions-output.txt")
+ );
+
+ Assert.Equal(["2.46.0", "2.47.0", "2.48.1"], versions);
+ }
+
+ [Fact]
+ public void InstallParametersIncludeChocolateySpecificFlagsAndCustomParameters()
+ {
+ var manager = new Chocolatey();
+ var package = new PackageBuilder().WithManager(manager).WithId("git").Build();
+ var options = new InstallOptions
+ {
+ InteractiveInstallation = true,
+ Architecture = Architecture.x86,
+ PreRelease = true,
+ SkipHashCheck = true,
+ Version = "2.48.1",
+ CustomParameters_Install = ["--install-arg"],
+ };
+
+ var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Install);
+
+ OperationAssert.HasParameters(
+ parameters,
+ "install",
+ "git",
+ "-y",
+ "--notsilent",
+ "--no-progress",
+ "--forcex86",
+ "--prerelease",
+ "--ignore-checksums",
+ "--force",
+ "--version=2.48.1",
+ "--allow-downgrade",
+ "--install-arg"
+ );
+ }
+
+ [Fact]
+ public void UninstallParametersUseUninstallVerbAndOnlyUninstallCustomParameters()
+ {
+ var manager = new Chocolatey();
+ var package = new PackageBuilder().WithManager(manager).WithId("git").Build();
+ var options = new InstallOptions
+ {
+ InteractiveInstallation = true,
+ SkipHashCheck = true,
+ CustomParameters_Install = ["--install-arg"],
+ CustomParameters_Uninstall = ["--remove-arg"],
+ };
+
+ var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Uninstall);
+
+ OperationAssert.HasParameters(
+ parameters,
+ "uninstall",
+ "git",
+ "-y",
+ "--notsilent",
+ "--remove-arg"
+ );
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(3010)]
+ [InlineData(1641)]
+ [InlineData(1614)]
+ [InlineData(1605)]
+ public void OperationResultTreatsChocolateySuccessCodesAsSuccess(int returnCode)
+ {
+ var manager = new Chocolatey();
+ var package = new PackageBuilder().WithManager(manager).Build();
+
+ var veredict = manager.OperationHelper.GetResult(
+ package,
+ OperationType.Install,
+ ["completed"],
+ returnCode
+ );
+
+ OperationAssert.HasVeredict(veredict, OperationVeredict.Success);
+ }
+
+ [Fact]
+ public void OperationResultPromotesElevationFailuresToAutoRetry()
+ {
+ var manager = new Chocolatey();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithOptions(new OverridenInstallationOptions(runAsAdministrator: false))
+ .Build();
+
+ var veredict = manager.OperationHelper.GetResult(
+ package,
+ OperationType.Install,
+ ["Access denied while installing package"],
+ 1
+ );
+
+ OperationAssert.HasVeredict(veredict, OperationVeredict.AutoRetry);
+ Assert.True(package.OverridenOptions.RunAsAdministrator);
+ }
+
+ [Fact]
+ public void OperationResultReturnsFailureWhenElevationWasAlreadyRequested()
+ {
+ var manager = new Chocolatey();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithOptions(new OverridenInstallationOptions(runAsAdministrator: true))
+ .Build();
+
+ var veredict = manager.OperationHelper.GetResult(
+ package,
+ OperationType.Install,
+ ["Access denied while installing package"],
+ 1
+ );
+
+ OperationAssert.HasVeredict(veredict, OperationVeredict.Failure);
+ }
+
+ private static string[] ReadFixtureLines(string relativePath)
+ {
+ return PackageEngineFixtureFiles.ReadAllText(relativePath).Replace("\r\n", "\n").Split('\n');
+ }
+}
+#endif
diff --git a/src/UniGetUI.PackageEngine.Tests/DotNetManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/DotNetManagerTests.cs
new file mode 100644
index 0000000000..d4972fe4e8
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/DotNetManagerTests.cs
@@ -0,0 +1,61 @@
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Managers.DotNetManager;
+using UniGetUI.PackageEngine.Structs;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class DotNetManagerTests
+{
+ [Fact]
+ public void ParseInstalledPackages_SkipsHeadersAndFalseRows()
+ {
+ var manager = new DotNet();
+ var packages = DotNet.ParseInstalledPackages(
+ [
+ "Package Id Version Commands",
+ "--------------------------------------",
+ "dotnetsay 2.1.7 dotnetsay",
+ " 1.0.0",
+ "try-convert 0.9.232202",
+ ],
+ manager.DefaultSource,
+ manager,
+ new OverridenInstallationOptions(PackageScope.Local)
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ Assert.Equal("dotnetsay", package.Id);
+ Assert.Equal("2.1.7", package.VersionString);
+ Assert.Equal(PackageScope.Local, package.OverridenOptions.Scope);
+ },
+ package =>
+ {
+ Assert.Equal("try-convert", package.Id);
+ Assert.Equal("0.9.232202", package.VersionString);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseInstalledPackages_PreservesRequestedScope()
+ {
+ var manager = new DotNet();
+ var package = Assert.Single(
+ DotNet.ParseInstalledPackages(
+ [
+ "Package Id Version Commands",
+ "--------------------------------------",
+ "dotnetsay 2.1.7 dotnetsay",
+ ],
+ manager.DefaultSource,
+ manager,
+ new OverridenInstallationOptions(PackageScope.Global)
+ )
+ );
+
+ Assert.Equal(PackageScope.Global, package.OverridenOptions.Scope);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/list-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/list-output.txt
new file mode 100644
index 0000000000..40c5661457
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/list-output.txt
@@ -0,0 +1,6 @@
+Chocolatey v2.4.3
+git 2.47.0
+Validation Failures:
+7zip 24.9.0
+Output is Id Version
+foo
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/outdated-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/outdated-output.txt
new file mode 100644
index 0000000000..6a5d552f23
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/outdated-output.txt
@@ -0,0 +1,6 @@
+Chocolatey v2.4.3
+git|2.47.0|2.48.1
+Validation|current version|available version
+7zip|24.9.0|25.0.0
+git|2.48.1|2.48.1
+foo|1.0
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/search-versions-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/search-versions-output.txt
new file mode 100644
index 0000000000..faeea04bad
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/search-versions-output.txt
@@ -0,0 +1,5 @@
+Chocolatey v2.4.3
+git 2.46.0 [Approved]
+git 2.47.0 [Approved]
+not-a-version line
+git 2.48.1 [Approved]
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/source-list-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/source-list-output.txt
new file mode 100644
index 0000000000..b28ad329d8
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Chocolatey/source-list-output.txt
@@ -0,0 +1,5 @@
+Chocolatey v2.4.3
+community - https://community.chocolatey.org/api/v2/ | Priority 0|Bypass Proxy - false|Self-Service - false|Admin Only - false
+legacy community - https://chocolatey.org/api/v2/ | Priority 0|Bypass Proxy - false|Self-Service - false|Admin Only - false
+internal repo - https://packages.example.test/api/v2/ | Priority 10|Bypass Proxy - false|Self-Service - false|Admin Only - false
+invalid source line
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/installed.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/installed.json
new file mode 100644
index 0000000000..e94d287472
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/installed.json
@@ -0,0 +1,11 @@
+{
+ "name": "workspace",
+ "dependencies": {
+ "rimraf": {
+ "version": "6.0.1"
+ },
+ "missing-version": {
+ "resolved": "https://registry.npmjs.org/missing-version"
+ }
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/outdated.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/outdated.json
new file mode 100644
index 0000000000..8153358b79
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/outdated.json
@@ -0,0 +1,11 @@
+{
+ "npm": {
+ "current": "10.9.0",
+ "wanted": "10.9.0",
+ "latest": "11.0.0",
+ "location": "C:\\Users\\tester\\AppData\\Roaming\\npm\\node_modules\\npm"
+ },
+ "missing-current": {
+ "latest": "1.0.0"
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/search-array-with-warning.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/search-array-with-warning.txt
new file mode 100644
index 0000000000..92d67450a1
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/search-array-with-warning.txt
@@ -0,0 +1,13 @@
+npm warn config global `--global`, `--local` are deprecated. Use `--location=global` instead.
+[
+ {
+ "name": "left-pad",
+ "version": "1.3.0",
+ "description": "String left pad"
+ },
+ {
+ "name": "@types/node",
+ "version": "24.0.0",
+ "description": "TypeScript definitions for Node.js"
+ }
+]
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/search-ndjson.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/search-ndjson.txt
new file mode 100644
index 0000000000..cea052c7bb
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Npm/search-ndjson.txt
@@ -0,0 +1,5 @@
+npm warn old lockfile
+{"name":"chalk","version":"5.4.1","description":"Terminal string styling done right"}
+not-json
+{"name":"missing-version"}
+{"name":"npm-check-updates","version":"17.1.1","description":"Find newer package dependencies"}
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/installed-list.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/installed-list.txt
new file mode 100644
index 0000000000..febd633a98
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/installed-list.txt
@@ -0,0 +1,6 @@
+Package Version
+------------------ ----------
+requests 2.31.0
+[notice] invalid
+django-reqtools 1.0.0
+DEPRECATION: ignored
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/outdated-list.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/outdated-list.txt
new file mode 100644
index 0000000000..2b6088ce3b
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/outdated-list.txt
@@ -0,0 +1,6 @@
+Package Version Latest Type
+------------------ ---------- ---------- -----
+requests 2.31.0 2.32.3 wheel
+[notice] invalid invalid wheel
+django-reqtools 1.0.0 1.1.0 wheel
+WARNING: ignored ignored wheel
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/simple-index.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/simple-index.json
new file mode 100644
index 0000000000..884cd9afff
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/simple-index.json
@@ -0,0 +1,12 @@
+{
+ "projects": [
+ { "name": "requests" },
+ { "name": "req" },
+ { "name": "requests-cache" },
+ { "name": "requestium" },
+ { "name": "django-reqtools" },
+ { "name": "pytest" },
+ { "name": null },
+ {}
+ ]
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/bucket-list-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/bucket-list-output.txt
new file mode 100644
index 0000000000..537cc44389
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/bucket-list-output.txt
@@ -0,0 +1,5 @@
+Name Source Updated Manifests
+---- ------ ------- ---------
+main https://github.com/ScoopInstaller/Main.git 2024-02-01 12:34:56 1234
+extras C:\Users\fixture\scoop\buckets\extras 2024-02-02 09:08:07 321
+invalid line
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/list-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/list-output.txt
new file mode 100644
index 0000000000..90cec9e5f3
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/list-output.txt
@@ -0,0 +1,5 @@
+Name Version Source
+---- ------- ------
+git 2.47.1 main
+pwsh 7.4.6 versions Global install
+No packages installed
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/search-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/search-output.txt
new file mode 100644
index 0000000000..c373032a12
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/search-output.txt
@@ -0,0 +1,6 @@
+'main' bucket:
+7zip 24.09
+WARN ignored
+'versions' bucket:
+python310 3.10.11
+No Matches Found
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/status-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/status-output.txt
new file mode 100644
index 0000000000..9ec066f9e8
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Scoop/status-output.txt
@@ -0,0 +1,6 @@
+Name Installed Latest
+---- --------- ------
+git 2.47.1 2.48.1
+pwsh 7.4.6 7.5.0
+orphan 0.1.0 0.2.0
+WARN failed removed
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/sample-manager-output.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/sample-manager-output.txt
new file mode 100644
index 0000000000..38d3959779
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/sample-manager-output.txt
@@ -0,0 +1,3 @@
+Name Id Version
+----------------------------------------
+Contoso Tool Contoso.Tool 1.2.3
diff --git a/src/UniGetUI.PackageEngine.Tests/HarnessSmokeTests.cs b/src/UniGetUI.PackageEngine.Tests/HarnessSmokeTests.cs
new file mode 100644
index 0000000000..84ee4dd3a2
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/HarnessSmokeTests.cs
@@ -0,0 +1,147 @@
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.PackageLoader;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class HarnessSmokeTests
+{
+ [Fact]
+ public void PackageManagerBuilderCreatesReadyManagerWithDeterministicSources()
+ {
+ var manager = new PackageManagerBuilder()
+ .WithName("Harness")
+ .WithDisplayName("Harness Manager")
+ .WithSources(manager =>
+ [
+ new SourceBuilder()
+ .WithManager(manager)
+ .WithName("community")
+ .WithUrl("https://example.test/community")
+ .WithPackageCount(42)
+ .Build(),
+ ])
+ .Build();
+
+ Assert.True(manager.IsReady());
+ Assert.Equal("Harness", manager.Name);
+ Assert.Equal("Harness Manager", manager.DisplayName);
+ var source = Assert.Single(manager.SourcesHelper.GetSources());
+ Assert.Equal("community", source.Name);
+ Assert.Same(source, manager.SourcesHelper.Factory.GetSourceIfExists("community"));
+ }
+
+ [Fact]
+ public async Task PackageDetailsBuilderPopulatesPackageThroughFakeHelper()
+ {
+ var manager = new PackageManagerBuilder()
+ .ConfigureDetails(helper =>
+ helper.DetailsFactory = package =>
+ new PackageDetailsBuilder()
+ .WithDescription("Deterministic package description")
+ .WithPublisher("Contoso")
+ .WithAuthor("UniGetUI Tests")
+ .WithHomepage("https://example.test/package")
+ .WithLicense("MIT", "https://example.test/license")
+ .WithInstaller(
+ "https://example.test/installer.exe",
+ "abc123",
+ "exe",
+ 2048
+ )
+ .WithManifest("https://example.test/manifest.json")
+ .WithReleaseNotes(
+ "Smoke-test release notes",
+ "https://example.test/release-notes"
+ )
+ .WithUpdateDate("2026-01-01")
+ .WithTag("smoke")
+ .WithDependency("dep.one", "1.0.0")
+ .Build(package))
+ .Build();
+ var package = new PackageBuilder().WithManager(manager).WithId("Contoso.Test").Build();
+
+ await package.Details.Load();
+
+ Assert.True(package.Details.IsPopulated);
+ Assert.Equal("Deterministic package description", package.Details.Description);
+ Assert.Equal("Contoso", package.Details.Publisher);
+ Assert.Single(package.Details.Dependencies);
+ }
+
+ [Fact]
+ public async Task InstalledPackagesLoaderLoadsConfiguredPackagesWithoutLiveProcesses()
+ {
+ var manager = new PackageManagerBuilder()
+ .WithInstalledPackages(manager =>
+ [
+ new PackageBuilder()
+ .WithManager(manager)
+ .WithName("Contoso Tool")
+ .WithId("Contoso.Tool")
+ .WithVersion("1.2.3")
+ .Build(),
+ ])
+ .Build();
+ _ = new DiscoverablePackagesLoader([manager]);
+ _ = new UpgradablePackagesLoader([manager]);
+ var loader = new InstalledPackagesLoader([manager]);
+ var recorder = new LoaderEventRecorder(loader);
+
+ await loader.ReloadPackagesSilently();
+
+ Assert.True(recorder.StartedLoading);
+ Assert.True(recorder.FinishedLoading);
+ var package = Assert.Single(loader.Packages);
+ PackageAssert.Matches(package, "Contoso Tool", "Contoso.Tool", "1.2.3");
+ Assert.Contains(recorder.AddedPackages, candidate => candidate.Id == "Contoso.Tool");
+ }
+
+ [Fact]
+ public void OperationHelperReturnsConfiguredParametersAndVeredict()
+ {
+ var manager = new PackageManagerBuilder()
+ .ConfigureOperation(helper =>
+ {
+ helper.ParametersFactory = (package, options, operation) =>
+ [
+ operation.ToString().ToLowerInvariant(),
+ package.Id,
+ "--scope",
+ options.InstallationScope,
+ ];
+ helper.ResultFactory = (_, _, _, returnCode) =>
+ returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure;
+ })
+ .Build();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithName("Contoso Tool")
+ .WithId("Contoso.Tool")
+ .Build();
+ var options = new InstallOptions { InstallationScope = PackageScope.User };
+
+ var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Install);
+ var veredict = manager.OperationHelper.GetResult(
+ package,
+ OperationType.Install,
+ ["completed"],
+ 0
+ );
+
+ OperationAssert.HasParameters(parameters, "install", "Contoso.Tool", "--scope", PackageScope.User);
+ OperationAssert.HasVeredict(veredict, OperationVeredict.Success);
+ }
+
+ [Fact]
+ public void FixtureHelperReadsSampleFixture()
+ {
+ var contents = PackageEngineFixtureFiles.ReadAllText("sample-manager-output.txt");
+
+ Assert.Contains("Contoso.Tool", contents);
+ Assert.Contains("1.2.3", contents);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/IgnoredUpdatesDatabaseTests.cs b/src/UniGetUI.PackageEngine.Tests/IgnoredUpdatesDatabaseTests.cs
new file mode 100644
index 0000000000..f334da0613
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/IgnoredUpdatesDatabaseTests.cs
@@ -0,0 +1,87 @@
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.Core.Tools;
+using UniGetUI.PackageEngine.Classes.Packages.Classes;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class IgnoredUpdatesDatabaseTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ Path.GetTempPath(),
+ nameof(IgnoredUpdatesDatabaseTests),
+ Guid.NewGuid().ToString("N")
+ );
+
+ public IgnoredUpdatesDatabaseTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Settings.ResetSettings();
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void AddGetAndRemoveRoundTripForSpecificVersions()
+ {
+ var manager = new PackageManagerBuilder().Build();
+ var package = new PackageBuilder().WithManager(manager).WithId("Contoso.Tool").Build();
+ string ignoredId = IgnoredUpdatesDatabase.GetIgnoredIdForPackage(package);
+
+ IgnoredUpdatesDatabase.Add(ignoredId, "2.0.0");
+
+ Assert.Equal("2.0.0", IgnoredUpdatesDatabase.GetIgnoredVersion(ignoredId));
+ Assert.True(IgnoredUpdatesDatabase.HasUpdatesIgnored(ignoredId, "2.0.0"));
+ Assert.False(IgnoredUpdatesDatabase.HasUpdatesIgnored(ignoredId, "3.0.0"));
+ Assert.True(IgnoredUpdatesDatabase.Remove(ignoredId));
+ Assert.Null(IgnoredUpdatesDatabase.GetIgnoredVersion(ignoredId));
+ }
+
+ [Fact]
+ public void HasUpdatesIgnored_HonorsWildcardAndExpiresPastDateEntries()
+ {
+ const string ignoredId = "manager\\contoso.tool";
+ string futureDate = $"<{DateTime.Today.AddDays(5):yyyy-MM-dd}";
+ string pastDate = $"<{DateTime.Today.AddDays(-5):yyyy-MM-dd}";
+
+ IgnoredUpdatesDatabase.Add(ignoredId, "*");
+ Assert.True(IgnoredUpdatesDatabase.HasUpdatesIgnored(ignoredId, "9.9.9"));
+
+ Settings.SetDictionary(Settings.K.IgnoredPackageUpdates, new Dictionary { [ignoredId] = futureDate });
+ Assert.True(IgnoredUpdatesDatabase.HasUpdatesIgnored(ignoredId, "1.0.0"));
+
+ Settings.SetDictionary(Settings.K.IgnoredPackageUpdates, new Dictionary { [ignoredId] = pastDate });
+ Assert.False(IgnoredUpdatesDatabase.HasUpdatesIgnored(ignoredId, "1.0.0"));
+ Assert.Null(IgnoredUpdatesDatabase.GetIgnoredVersion(ignoredId));
+ }
+
+ [Theory]
+ [InlineData(14)]
+ [InlineData(30)]
+ [InlineData(365)]
+ public void PauseTime_StringRepresentationUsesLargestFriendlyUnit(int days)
+ {
+ var pauseTime = new IgnoredUpdatesDatabase.PauseTime { Days = days };
+
+ string expected = days switch
+ {
+ 14 => CoreTools.Translate("{0} weeks", 2),
+ 30 => CoreTools.Translate("1 month"),
+ 365 => CoreTools.Translate("{0} months", 13),
+ _ => throw new InvalidOperationException("Unexpected test input"),
+ };
+
+ Assert.Equal(expected, pauseTime.StringRepresentation());
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Assertions/OperationAssert.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Assertions/OperationAssert.cs
new file mode 100644
index 0000000000..f031ee94dc
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Assertions/OperationAssert.cs
@@ -0,0 +1,16 @@
+using UniGetUI.PackageEngine.Enums;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+
+public static class OperationAssert
+{
+ public static void HasVeredict(OperationVeredict actual, OperationVeredict expected)
+ {
+ Assert.Equal(expected, actual);
+ }
+
+ public static void HasParameters(IReadOnlyList actual, params string[] expected)
+ {
+ Assert.Equal(expected, actual);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Assertions/PackageAssert.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Assertions/PackageAssert.cs
new file mode 100644
index 0000000000..5ea81ab65f
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Assertions/PackageAssert.cs
@@ -0,0 +1,25 @@
+using UniGetUI.PackageEngine.Interfaces;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+
+public static class PackageAssert
+{
+ public static void Matches(IPackage package, string name, string id, string version, string? newVersion = null)
+ {
+ Assert.Equal(name, package.Name);
+ Assert.Equal(id, package.Id);
+ Assert.Equal(version, package.VersionString);
+ Assert.Equal(newVersion ?? version, package.NewVersionString);
+ }
+
+ public static void BelongsTo(IPackage package, IPackageManager manager, IManagerSource source)
+ {
+ Assert.Same(manager, package.Manager);
+ Assert.Same(source, package.Source);
+ }
+
+ public static void ContainsIds(IEnumerable packages, params string[] expectedIds)
+ {
+ Assert.Equal(expectedIds.OrderBy(id => id), packages.Select(package => package.Id).OrderBy(id => id));
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageBuilder.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageBuilder.cs
new file mode 100644
index 0000000000..96ace5f14e
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageBuilder.cs
@@ -0,0 +1,68 @@
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.PackageClasses;
+using UniGetUI.PackageEngine.Structs;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+
+public sealed class PackageBuilder
+{
+ private string _name = "Test Package";
+ private string _id = "Contoso.Test";
+ private string _installedVersion = "1.0.0";
+ private string? _availableVersion;
+ private IManagerSource? _source;
+ private IPackageManager? _manager;
+ private OverridenInstallationOptions? _options;
+
+ public PackageBuilder WithName(string name)
+ {
+ _name = name;
+ return this;
+ }
+
+ public PackageBuilder WithId(string id)
+ {
+ _id = id;
+ return this;
+ }
+
+ public PackageBuilder WithVersion(string version)
+ {
+ _installedVersion = version;
+ return this;
+ }
+
+ public PackageBuilder WithNewVersion(string version)
+ {
+ _availableVersion = version;
+ return this;
+ }
+
+ public PackageBuilder WithManager(IPackageManager manager)
+ {
+ _manager = manager;
+ return this;
+ }
+
+ public PackageBuilder WithSource(IManagerSource source)
+ {
+ _source = source;
+ return this;
+ }
+
+ public PackageBuilder WithOptions(OverridenInstallationOptions options)
+ {
+ _options = options;
+ return this;
+ }
+
+ public Package Build()
+ {
+ var manager = _manager ?? new PackageManagerBuilder().Build();
+ var source = _source ?? manager.DefaultSource;
+
+ return _availableVersion is null
+ ? new Package(_name, _id, _installedVersion, source, manager, _options)
+ : new Package(_name, _id, _installedVersion, _availableVersion, source, manager, _options);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageDetailsBuilder.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageDetailsBuilder.cs
new file mode 100644
index 0000000000..37f92c7c4a
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageDetailsBuilder.cs
@@ -0,0 +1,125 @@
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.PackageClasses;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+
+public sealed class PackageDetailsBuilder
+{
+ private string? _description;
+ private string? _publisher;
+ private string? _author;
+ private Uri? _homepageUrl;
+ private string? _license;
+ private Uri? _licenseUrl;
+ private Uri? _installerUrl;
+ private string? _installerHash;
+ private string? _installerType;
+ private long _installerSize;
+ private Uri? _manifestUrl;
+ private string? _updateDate;
+ private string? _releaseNotes;
+ private Uri? _releaseNotesUrl;
+ private readonly List _tags = [];
+ private readonly List _dependencies = [];
+
+ public PackageDetailsBuilder WithDescription(string description)
+ {
+ _description = description;
+ return this;
+ }
+
+ public PackageDetailsBuilder WithPublisher(string publisher)
+ {
+ _publisher = publisher;
+ return this;
+ }
+
+ public PackageDetailsBuilder WithAuthor(string author)
+ {
+ _author = author;
+ return this;
+ }
+
+ public PackageDetailsBuilder WithHomepage(string homepageUrl)
+ {
+ _homepageUrl = new Uri(homepageUrl);
+ return this;
+ }
+
+ public PackageDetailsBuilder WithLicense(string license, string? licenseUrl = null)
+ {
+ _license = license;
+ _licenseUrl = licenseUrl is null ? null : new Uri(licenseUrl);
+ return this;
+ }
+
+ public PackageDetailsBuilder WithInstaller(string installerUrl, string hash, string installerType, long installerSize)
+ {
+ _installerUrl = new Uri(installerUrl);
+ _installerHash = hash;
+ _installerType = installerType;
+ _installerSize = installerSize;
+ return this;
+ }
+
+ public PackageDetailsBuilder WithManifest(string manifestUrl)
+ {
+ _manifestUrl = new Uri(manifestUrl);
+ return this;
+ }
+
+ public PackageDetailsBuilder WithReleaseNotes(string releaseNotes, string? releaseNotesUrl = null)
+ {
+ _releaseNotes = releaseNotes;
+ _releaseNotesUrl = releaseNotesUrl is null ? null : new Uri(releaseNotesUrl);
+ return this;
+ }
+
+ public PackageDetailsBuilder WithUpdateDate(string updateDate)
+ {
+ _updateDate = updateDate;
+ return this;
+ }
+
+ public PackageDetailsBuilder WithTag(string tag)
+ {
+ _tags.Add(tag);
+ return this;
+ }
+
+ public PackageDetailsBuilder WithDependency(string name, string version, bool mandatory = true)
+ {
+ _dependencies.Add(
+ new IPackageDetails.Dependency
+ {
+ Name = name,
+ Version = version,
+ Mandatory = mandatory,
+ }
+ );
+ return this;
+ }
+
+ public PackageDetails Build(IPackage package)
+ {
+ return new PackageDetails(package)
+ {
+ Description = _description,
+ Publisher = _publisher,
+ Author = _author,
+ HomepageUrl = _homepageUrl,
+ License = _license,
+ LicenseUrl = _licenseUrl,
+ InstallerUrl = _installerUrl,
+ InstallerHash = _installerHash,
+ InstallerType = _installerType,
+ InstallerSize = _installerSize,
+ ManifestUrl = _manifestUrl,
+ UpdateDate = _updateDate,
+ ReleaseNotes = _releaseNotes,
+ ReleaseNotesUrl = _releaseNotesUrl,
+ Tags = _tags.ToArray(),
+ Dependencies = _dependencies.ToList(),
+ };
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageManagerBuilder.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageManagerBuilder.cs
new file mode 100644
index 0000000000..36a611b4e5
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/PackageManagerBuilder.cs
@@ -0,0 +1,120 @@
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.ManagerClasses.Manager;
+using UniGetUI.PackageEngine.PackageClasses;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+
+public sealed class PackageManagerBuilder
+{
+ private string _name = "TestManager";
+ private string _displayName = "Test Manager";
+ private Func _capabilitiesFactory = static capabilities => capabilities;
+ private Func> _sourcesFactory = manager => [manager.DefaultSource];
+ private Func> _availableUpdatesFactory = static _ => [];
+ private Func> _installedPackagesFactory = static _ => [];
+ private Action? _detailsConfiguration;
+ private Action? _operationConfiguration;
+ private Action? _sourceConfiguration;
+ private Action? _managerConfiguration;
+
+ public PackageManagerBuilder WithName(string name)
+ {
+ _name = name;
+ return this;
+ }
+
+ public PackageManagerBuilder WithDisplayName(string displayName)
+ {
+ _displayName = displayName;
+ return this;
+ }
+
+ public PackageManagerBuilder ConfigureCapabilities(
+ Func capabilitiesFactory
+ )
+ {
+ _capabilitiesFactory = capabilitiesFactory;
+ return this;
+ }
+
+ public PackageManagerBuilder WithSources(
+ Func> sourcesFactory
+ )
+ {
+ _sourcesFactory = sourcesFactory;
+ return this;
+ }
+
+ public PackageManagerBuilder WithFindPackages(
+ Func> findPackagesFactory
+ )
+ {
+ _managerConfiguration += manager => manager.SetFindPackages(query => findPackagesFactory(manager, query));
+ return this;
+ }
+
+ public PackageManagerBuilder WithInstalledPackages(
+ Func> installedPackagesFactory
+ )
+ {
+ _installedPackagesFactory = installedPackagesFactory;
+ return this;
+ }
+
+ public PackageManagerBuilder WithAvailableUpdates(
+ Func> availableUpdatesFactory
+ )
+ {
+ _availableUpdatesFactory = availableUpdatesFactory;
+ return this;
+ }
+
+ public PackageManagerBuilder ConfigureDetails(Action configure)
+ {
+ _detailsConfiguration = configure;
+ return this;
+ }
+
+ public PackageManagerBuilder ConfigureOperation(Action configure)
+ {
+ _operationConfiguration = configure;
+ return this;
+ }
+
+ public PackageManagerBuilder ConfigureSources(Action configure)
+ {
+ _sourceConfiguration = configure;
+ return this;
+ }
+
+ public PackageManagerBuilder ConfigureManager(Action configure)
+ {
+ _managerConfiguration += configure;
+ return this;
+ }
+
+ public TestPackageManager Build(bool initialize = true)
+ {
+ var manager = new TestPackageManager(_name, _displayName);
+ manager.Capabilities = _capabilitiesFactory(manager.Capabilities);
+
+ _detailsConfiguration?.Invoke(manager.TestDetailsHelper);
+ _operationConfiguration?.Invoke(manager.TestOperationHelper);
+ _sourceConfiguration?.Invoke(manager.TestSourcesHelper);
+
+ manager.SetKnownSources(_sourcesFactory(manager));
+ manager.SetFindPackages(_ => []);
+ manager.SetAvailableUpdates(() => _availableUpdatesFactory(manager));
+ manager.SetInstalledPackages(() => _installedPackagesFactory(manager));
+
+ _managerConfiguration?.Invoke(manager);
+
+ if (initialize)
+ {
+ manager.Initialize();
+ }
+
+ return manager;
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/SourceBuilder.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/SourceBuilder.cs
new file mode 100644
index 0000000000..231ac96835
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Builders/SourceBuilder.cs
@@ -0,0 +1,63 @@
+using UniGetUI.PackageEngine.Classes.Manager;
+using UniGetUI.PackageEngine.Interfaces;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+
+public sealed class SourceBuilder
+{
+ private IPackageManager? _manager;
+ private string _name = "default";
+ private Uri _url = new("https://example.test/default");
+ private int? _packageCount = 0;
+ private string _updateDate = string.Empty;
+ private bool _isVirtualManager;
+
+ public SourceBuilder WithManager(IPackageManager manager)
+ {
+ _manager = manager;
+ return this;
+ }
+
+ public SourceBuilder WithName(string name)
+ {
+ _name = name;
+ return this;
+ }
+
+ public SourceBuilder WithUrl(string url)
+ {
+ _url = new Uri(url);
+ return this;
+ }
+
+ public SourceBuilder WithPackageCount(int? packageCount)
+ {
+ _packageCount = packageCount;
+ return this;
+ }
+
+ public SourceBuilder WithUpdateDate(string updateDate)
+ {
+ _updateDate = updateDate;
+ return this;
+ }
+
+ public SourceBuilder AsVirtualManager(bool isVirtualManager = true)
+ {
+ _isVirtualManager = isVirtualManager;
+ return this;
+ }
+
+ public ManagerSource Build()
+ {
+ var manager = _manager ?? new PackageManagerBuilder().Build();
+ return new ManagerSource(
+ manager,
+ _name,
+ _url,
+ _packageCount,
+ _updateDate,
+ _isVirtualManager
+ );
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageDetailsHelper.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageDetailsHelper.cs
new file mode 100644
index 0000000000..b2b951b37a
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageDetailsHelper.cs
@@ -0,0 +1,71 @@
+using UniGetUI.Core.IconEngine;
+using UniGetUI.PackageEngine.Classes.Manager.BaseProviders;
+using UniGetUI.PackageEngine.Interfaces;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+public sealed class TestPackageDetailsHelper(TestPackageManager manager) : BasePkgDetailsHelper(manager)
+{
+ public Func? DetailsFactory { get; set; }
+
+ public Action? PopulateDetails { get; set; }
+
+ public Func> VersionsFactory { get; set; } = static _ => [];
+
+ public Func IconFactory { get; set; } = static _ => null;
+
+ public Func> ScreenshotsFactory { get; set; } = static _ => [];
+
+ public Func InstallLocationFactory { get; set; } = static _ => null;
+
+ protected override void GetDetails_UnSafe(IPackageDetails details)
+ {
+ if (DetailsFactory?.Invoke(details.Package) is { } source)
+ {
+ Copy(source, details);
+ }
+
+ PopulateDetails?.Invoke(details);
+ }
+
+ protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package)
+ {
+ return VersionsFactory(package);
+ }
+
+ protected override CacheableIcon? GetIcon_UnSafe(IPackage package)
+ {
+ return IconFactory(package);
+ }
+
+ protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package)
+ {
+ return ScreenshotsFactory(package);
+ }
+
+ protected override string? GetInstallLocation_UnSafe(IPackage package)
+ {
+ return InstallLocationFactory(package);
+ }
+
+ private static void Copy(IPackageDetails source, IPackageDetails target)
+ {
+ target.Description = source.Description;
+ target.Publisher = source.Publisher;
+ target.Author = source.Author;
+ target.HomepageUrl = source.HomepageUrl;
+ target.License = source.License;
+ target.LicenseUrl = source.LicenseUrl;
+ target.InstallerUrl = source.InstallerUrl;
+ target.InstallerHash = source.InstallerHash;
+ target.InstallerType = source.InstallerType;
+ target.InstallerSize = source.InstallerSize;
+ target.ManifestUrl = source.ManifestUrl;
+ target.UpdateDate = source.UpdateDate;
+ target.ReleaseNotes = source.ReleaseNotes;
+ target.ReleaseNotesUrl = source.ReleaseNotesUrl;
+ target.Tags = source.Tags.ToArray();
+ target.Dependencies.Clear();
+ target.Dependencies.AddRange(source.Dependencies);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageLoader.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageLoader.cs
new file mode 100644
index 0000000000..e64af173ea
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageLoader.cs
@@ -0,0 +1,49 @@
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.PackageLoader;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+internal sealed class TestPackageLoader : AbstractPackageLoader
+{
+ private readonly Func> _loadPackages;
+ private readonly Func> _isPackageValid;
+ private readonly Func _whenAddingPackage;
+
+ public TestPackageLoader(
+ IReadOnlyList managers,
+ bool allowMultiplePackageVersions = false,
+ bool disableReload = false,
+ bool checkedByDefault = false,
+ Func>? loadPackages = null,
+ Func>? isPackageValid = null,
+ Func? whenAddingPackage = null
+ )
+ : base(
+ managers,
+ identifier: "TEST_LOADER",
+ AllowMultiplePackageVersions: allowMultiplePackageVersions,
+ DisableReload: disableReload,
+ CheckedBydefault: checkedByDefault,
+ RequiresInternet: false
+ )
+ {
+ _loadPackages = loadPackages ?? (manager => manager.GetInstalledPackages());
+ _isPackageValid = isPackageValid ?? (_ => Task.FromResult(true));
+ _whenAddingPackage = whenAddingPackage ?? (_ => Task.CompletedTask);
+ }
+
+ protected override IReadOnlyList LoadPackagesFromManager(IPackageManager manager)
+ {
+ return _loadPackages(manager);
+ }
+
+ protected override Task IsPackageValid(IPackage package)
+ {
+ return _isPackageValid(package);
+ }
+
+ protected override Task WhenAddingPackage(IPackage package)
+ {
+ return _whenAddingPackage(package);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs
new file mode 100644
index 0000000000..a8ee96dc42
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs
@@ -0,0 +1,175 @@
+using UniGetUI.Interface.Enums;
+using UniGetUI.PackageEngine.Classes.Manager;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.ManagerClasses.Manager;
+using UniGetUI.PackageEngine.PackageClasses;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+public sealed class TestPackageManager : PackageManager
+{
+ private Func> _findPackages = _ => [];
+ private Func> _getAvailableUpdates = static () => [];
+ private Func> _getInstalledPackages = static () => [];
+ private IReadOnlyList _candidateExecutableFiles;
+
+ public TestPackageManager(string name = "TestManager", string? displayName = null)
+ {
+ Capabilities = new ManagerCapabilities
+ {
+ CanRunAsAdmin = true,
+ CanSkipIntegrityChecks = true,
+ CanRunInteractively = true,
+ CanRemoveDataOnUninstall = true,
+ CanDownloadInstaller = true,
+ CanUninstallPreviousVersionsAfterUpdate = true,
+ CanListDependencies = true,
+ SupportsCustomVersions = true,
+ SupportsCustomArchitectures = true,
+ SupportsCustomScopes = true,
+ SupportsPreRelease = true,
+ SupportsCustomLocations = true,
+ SupportsCustomSources = true,
+ SupportsCustomPackageIcons = true,
+ SupportsCustomPackageScreenshots = true,
+ SupportsProxy = ProxySupport.Yes,
+ SupportsProxyAuth = true,
+ Sources = new SourceCapabilities
+ {
+ KnowsPackageCount = true,
+ KnowsUpdateDate = true,
+ },
+ };
+
+ Properties = new ManagerProperties
+ {
+ Name = name,
+ DisplayName = displayName ?? name,
+ Description = "Package-engine test harness manager",
+ ExecutableFriendlyName = $"{name}.exe",
+ InstallVerb = "install",
+ UpdateVerb = "update",
+ UninstallVerb = "uninstall",
+ ColorIconId = "package_mask",
+ IconId = (IconType)'\uE8FD',
+ };
+
+ var defaultSource = new ManagerSource(this, "default", new Uri("https://example.test/default"));
+ var properties = Properties;
+ properties.DefaultSource = defaultSource;
+ properties.KnownSources = [defaultSource];
+ Properties = properties;
+
+ TestDetailsHelper = new TestPackageDetailsHelper(this);
+ TestOperationHelper = new TestPackageOperationHelper(this);
+ TestSourcesHelper = new TestSourceHelper(this);
+
+ DetailsHelper = TestDetailsHelper;
+ OperationHelper = TestOperationHelper;
+ SourcesHelper = TestSourcesHelper;
+
+ _candidateExecutableFiles = [$"C:\\test-tools\\{name}.exe"];
+ }
+
+ public TestPackageDetailsHelper TestDetailsHelper { get; }
+
+ public TestPackageOperationHelper TestOperationHelper { get; }
+
+ public TestSourceHelper TestSourcesHelper { get; }
+
+ public bool ExecutableFound { get; set; } = true;
+
+ public string ExecutablePath { get; set; } = "C:\\test-tools\\manager.exe";
+
+ public string ExecutableArguments { get; set; } = "";
+
+ public string LoadedVersion { get; set; } = "1.0.0-test";
+
+ public int AttemptFastRepairCalls { get; private set; }
+
+ public int RefreshPackageIndexesCalls { get; private set; }
+
+ public string? LastQuery { get; private set; }
+
+ public void SetFindPackages(Func> findPackages)
+ {
+ _findPackages = findPackages;
+ }
+
+ public void SetAvailableUpdates(Func> getAvailableUpdates)
+ {
+ _getAvailableUpdates = getAvailableUpdates;
+ }
+
+ public void SetInstalledPackages(Func> getInstalledPackages)
+ {
+ _getInstalledPackages = getInstalledPackages;
+ }
+
+ public void SetCandidateExecutableFiles(params string[] candidateExecutableFiles)
+ {
+ _candidateExecutableFiles = candidateExecutableFiles;
+ }
+
+ public void SetKnownSources(IEnumerable sources)
+ {
+ var sourceArray = sources.ToArray();
+ if (sourceArray.Length == 0)
+ {
+ sourceArray = [DefaultSource];
+ }
+
+ TestSourcesHelper.SetSources(sourceArray);
+
+ var properties = Properties;
+ properties.KnownSources = sourceArray;
+ Properties = properties;
+ }
+
+ protected override void _loadManagerExecutableFile(
+ out bool found,
+ out string path,
+ out string callArguments
+ )
+ {
+ found = ExecutableFound;
+ path = ExecutablePath;
+ callArguments = ExecutableArguments;
+ }
+
+ protected override void _loadManagerVersion(out string version)
+ {
+ version = LoadedVersion;
+ }
+
+ protected override IReadOnlyList FindPackages_UnSafe(string query)
+ {
+ LastQuery = query;
+ return _findPackages(query);
+ }
+
+ protected override IReadOnlyList GetAvailableUpdates_UnSafe()
+ {
+ return _getAvailableUpdates();
+ }
+
+ protected override IReadOnlyList GetInstalledPackages_UnSafe()
+ {
+ return _getInstalledPackages();
+ }
+
+ public override IReadOnlyList FindCandidateExecutableFiles()
+ {
+ return _candidateExecutableFiles;
+ }
+
+ public override void AttemptFastRepair()
+ {
+ AttemptFastRepairCalls++;
+ }
+
+ public override void RefreshPackageIndexes()
+ {
+ RefreshPackageIndexesCalls++;
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageOperationHelper.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageOperationHelper.cs
new file mode 100644
index 0000000000..74a9c33079
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageOperationHelper.cs
@@ -0,0 +1,35 @@
+using UniGetUI.PackageEngine.Classes.Manager.BaseProviders;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.Serializable;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+public sealed class TestPackageOperationHelper(TestPackageManager manager)
+ : BasePkgOperationHelper(manager)
+{
+ public Func> ParametersFactory { get; set; } =
+ static (_, _, _) => [];
+
+ public Func, int, OperationVeredict> ResultFactory { get; set; } =
+ static (_, _, _, returnCode) => returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure;
+
+ protected override IReadOnlyList _getOperationParameters(
+ IPackage package,
+ InstallOptions options,
+ OperationType operation
+ )
+ {
+ return ParametersFactory(package, options, operation);
+ }
+
+ protected override OperationVeredict _getOperationResult(
+ IPackage package,
+ OperationType operation,
+ IReadOnlyList processOutput,
+ int returnCode
+ )
+ {
+ return ResultFactory(package, operation, processOutput, returnCode);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestSourceHelper.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestSourceHelper.cs
new file mode 100644
index 0000000000..8f39f1b275
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestSourceHelper.cs
@@ -0,0 +1,60 @@
+using UniGetUI.PackageEngine.Classes.Manager.Providers;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+public sealed class TestSourceHelper(TestPackageManager manager) : BaseSourceHelper(manager)
+{
+ private IReadOnlyList _sources = [manager.DefaultSource];
+
+ public Func AddParametersFactory { get; set; } =
+ static source => ["source", "add", source.Name];
+
+ public Func RemoveParametersFactory { get; set; } =
+ static source => ["source", "remove", source.Name];
+
+ public Func AddVeredictFactory { get; set; } =
+ static (_, returnCode, _) => returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure;
+
+ public Func RemoveVeredictFactory { get; set; } =
+ static (_, returnCode, _) => returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure;
+
+ public void SetSources(IEnumerable sources)
+ {
+ _sources = sources.ToArray();
+ }
+
+ public override string[] GetAddSourceParameters(IManagerSource source)
+ {
+ return AddParametersFactory(source);
+ }
+
+ public override string[] GetRemoveSourceParameters(IManagerSource source)
+ {
+ return RemoveParametersFactory(source);
+ }
+
+ protected override OperationVeredict _getAddSourceOperationVeredict(
+ IManagerSource source,
+ int ReturnCode,
+ string[] Output
+ )
+ {
+ return AddVeredictFactory(source, ReturnCode, Output);
+ }
+
+ protected override OperationVeredict _getRemoveSourceOperationVeredict(
+ IManagerSource source,
+ int ReturnCode,
+ string[] Output
+ )
+ {
+ return RemoveVeredictFactory(source, ReturnCode, Output);
+ }
+
+ protected override IReadOnlyList GetSources_UnSafe()
+ {
+ return _sources;
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestUpgradablePackagesLoader.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestUpgradablePackagesLoader.cs
new file mode 100644
index 0000000000..b68e9158af
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestUpgradablePackagesLoader.cs
@@ -0,0 +1,25 @@
+using System.Reflection;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.PackageLoader;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+internal sealed class TestUpgradablePackagesLoader : UpgradablePackagesLoader
+{
+ private static readonly FieldInfo UpdatesTimerField = typeof(UpgradablePackagesLoader).GetField(
+ "UpdatesTimer",
+ BindingFlags.Instance | BindingFlags.NonPublic
+ )!;
+
+ public TestUpgradablePackagesLoader(IReadOnlyList managers)
+ : base(managers) { }
+
+ public Task EvaluatePackageAsync(IPackage package) => IsPackageValid(package);
+
+ public Task ApplyWhenAddingPackageAsync(IPackage package) => WhenAddingPackage(package);
+
+ public void StartTimer() => StartAutoCheckTimeout();
+
+ public double? GetTimerIntervalMilliseconds() =>
+ (UpdatesTimerField.GetValue(this) as System.Timers.Timer)?.Interval;
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/LoaderEventRecorder.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/LoaderEventRecorder.cs
new file mode 100644
index 0000000000..1d4fbc52a7
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/LoaderEventRecorder.cs
@@ -0,0 +1,37 @@
+using UniGetUI.PackageEngine.PackageLoader;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+public sealed class LoaderEventRecorder
+{
+ public LoaderEventRecorder(AbstractPackageLoader loader)
+ {
+ loader.StartedLoading += (_, _) =>
+ {
+ StartedLoading = true;
+ StartedLoadingCount++;
+ };
+ loader.FinishedLoading += (_, _) =>
+ {
+ FinishedLoading = true;
+ FinishedLoadingCount++;
+ };
+ loader.PackagesChanged += (_, args) => Changes.Add(args);
+ }
+
+ public bool StartedLoading { get; private set; }
+
+ public int StartedLoadingCount { get; private set; }
+
+ public bool FinishedLoading { get; private set; }
+
+ public int FinishedLoadingCount { get; private set; }
+
+ public List Changes { get; } = [];
+
+ public IReadOnlyList AddedPackages =>
+ Changes.SelectMany(change => change.AddedPackages).ToArray();
+
+ public IReadOnlyList RemovedPackages =>
+ Changes.SelectMany(change => change.RemovedPackages).ToArray();
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/PackageEngineFixtureFiles.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/PackageEngineFixtureFiles.cs
new file mode 100644
index 0000000000..e8971042ac
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/PackageEngineFixtureFiles.cs
@@ -0,0 +1,23 @@
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+public static class PackageEngineFixtureFiles
+{
+ public static string RootPath => Path.Combine(AppContext.BaseDirectory, "Fixtures");
+
+ public static string GetPath(string relativePath)
+ {
+ var fullPath = Path.Combine(RootPath, relativePath);
+ Assert.True(File.Exists(fullPath), $"Expected fixture file to exist: {fullPath}");
+ return fullPath;
+ }
+
+ public static string ReadAllText(string relativePath)
+ {
+ return File.ReadAllText(GetPath(relativePath));
+ }
+
+ public static byte[] ReadAllBytes(string relativePath)
+ {
+ return File.ReadAllBytes(GetPath(relativePath));
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/TestHttpServer.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/TestHttpServer.cs
new file mode 100644
index 0000000000..e3e731f47d
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/TestHttpServer.cs
@@ -0,0 +1,87 @@
+using System.Net;
+using System.Net.Sockets;
+
+namespace UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+internal sealed class TestHttpServer : IDisposable
+{
+ private readonly HttpListener _listener = new();
+ private readonly Func _handler;
+ private readonly List _requestPaths = [];
+ private readonly Task _backgroundTask;
+
+ public TestHttpServer(
+ Func handler
+ )
+ {
+ _handler = handler;
+ int port = GetAvailablePort();
+ BaseUri = new Uri($"http://127.0.0.1:{port}/");
+ _listener.Prefixes.Add(BaseUri.AbsoluteUri);
+ _listener.Start();
+ _backgroundTask = Task.Run(ListenAsync);
+ }
+
+ public Uri BaseUri { get; }
+
+ public IReadOnlyList RequestPaths
+ {
+ get
+ {
+ lock (_requestPaths)
+ {
+ return _requestPaths.ToArray();
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_listener.IsListening)
+ {
+ _listener.Stop();
+ }
+
+ _listener.Close();
+ _backgroundTask.GetAwaiter().GetResult();
+ }
+
+ private async Task ListenAsync()
+ {
+ while (true)
+ {
+ try
+ {
+ HttpListenerContext context = await _listener.GetContextAsync();
+
+ lock (_requestPaths)
+ {
+ _requestPaths.Add(context.Request.RawUrl ?? context.Request.Url?.AbsolutePath ?? string.Empty);
+ }
+
+ var (statusCode, content, contentType) = _handler(context.Request);
+ context.Response.StatusCode = statusCode;
+ context.Response.ContentType = contentType;
+ using StreamWriter writer = new(context.Response.OutputStream);
+ await writer.WriteAsync(content);
+ await writer.FlushAsync();
+ context.Response.Close();
+ }
+ catch (HttpListenerException)
+ {
+ break;
+ }
+ catch (ObjectDisposedException)
+ {
+ break;
+ }
+ }
+ }
+
+ private static int GetAvailablePort()
+ {
+ using TcpListener listener = new(IPAddress.Loopback, 0);
+ listener.Start();
+ return ((IPEndPoint)listener.LocalEndpoint).Port;
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/InstallOptionsFactoryTests.cs b/src/UniGetUI.PackageEngine.Tests/InstallOptionsFactoryTests.cs
new file mode 100644
index 0000000000..ef63fad7ba
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/InstallOptionsFactoryTests.cs
@@ -0,0 +1,164 @@
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.Core.SettingsEngine.SecureSettings;
+using UniGetUI.Core.Tools;
+using UniGetUI.PackageEngine.Classes.Packages;
+using UniGetUI.PackageEngine.Classes.Packages.Classes;
+using UniGetUI.PackageEngine.PackageClasses;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class InstallOptionsFactoryTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ Path.GetTempPath(),
+ nameof(InstallOptionsFactoryTests),
+ Guid.NewGuid().ToString("N")
+ );
+
+ public InstallOptionsFactoryTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ SecureSettings.TEST_SecureSettingsRootOverride = Path.Combine(_testRoot, "SecureSettings");
+ Directory.CreateDirectory(CoreData.UniGetUIInstallationOptionsDirectory);
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Settings.ResetSettings();
+ SecureSettings.ApplyForUser(
+ Environment.UserName,
+ SecureSettings.ResolveKey(SecureSettings.K.AllowCLIArguments),
+ false
+ );
+ SecureSettings.ApplyForUser(
+ Environment.UserName,
+ SecureSettings.ResolveKey(SecureSettings.K.AllowPrePostOpCommand),
+ false
+ );
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ SecureSettings.TEST_SecureSettingsRootOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void LoadApplicable_UsesManagerDefaultsAndExpandsPackageToken()
+ {
+ var manager = new PackageManagerBuilder().WithName($"Manager{Guid.NewGuid():N}").Build();
+ var package = new PackageBuilder().WithManager(manager).WithId("Contoso:Tool").Build();
+ var managerOptions = new InstallOptions
+ {
+ CustomInstallLocation = @"C:\Apps\%PACKAGE%",
+ InteractiveInstallation = true,
+ };
+
+ InstallOptionsFactory.SaveForManager(managerOptions, manager);
+ InstallOptionsFactory.SaveForPackage(new InstallOptions(), package);
+
+ var resolved = InstallOptionsFactory.LoadApplicable(package);
+
+ Assert.Equal(
+ $@"C:\Apps\{CoreTools.MakeValidFileName(package.Id)}",
+ resolved.CustomInstallLocation
+ );
+ Assert.True(resolved.InteractiveInstallation);
+ }
+
+ [Fact]
+ public void LoadApplicable_AppliesExplicitOverridesAndRemovesDisallowedSecureOptions()
+ {
+ var manager = new PackageManagerBuilder().WithName($"Manager{Guid.NewGuid():N}").Build();
+ var package = new PackageBuilder().WithManager(manager).WithId($"Pkg{Guid.NewGuid():N}").Build();
+ var packageOptions = new InstallOptions
+ {
+ OverridesNextLevelOpts = true,
+ CustomParameters_Install = ["--keep&drop|;<>\n"],
+ CustomParameters_Update = ["--update"],
+ CustomParameters_Uninstall = ["--remove"],
+ PreInstallCommand = "echo pre",
+ PostInstallCommand = "echo post",
+ PreUpdateCommand = "echo pre-update",
+ PostUpdateCommand = "echo post-update",
+ PreUninstallCommand = "echo pre-uninstall",
+ PostUninstallCommand = "echo post-uninstall",
+ };
+
+ InstallOptionsFactory.SaveForPackage(packageOptions, package);
+
+ var resolved = InstallOptionsFactory.LoadApplicable(
+ package,
+ elevated: true,
+ interactive: true,
+ no_integrity: true,
+ remove_data: true
+ );
+
+ Assert.True(resolved.RunAsAdministrator);
+ Assert.True(resolved.InteractiveInstallation);
+ Assert.True(resolved.SkipHashCheck);
+ Assert.True(resolved.RemoveDataOnUninstall);
+ Assert.Empty(resolved.CustomParameters_Install);
+ Assert.Empty(resolved.CustomParameters_Update);
+ Assert.Empty(resolved.CustomParameters_Uninstall);
+ Assert.Equal("", resolved.PreInstallCommand);
+ Assert.Equal("", resolved.PostInstallCommand);
+ Assert.Equal("", resolved.PreUpdateCommand);
+ Assert.Equal("", resolved.PostUpdateCommand);
+ Assert.Equal("", resolved.PreUninstallCommand);
+ Assert.Equal("", resolved.PostUninstallCommand);
+ }
+
+ [Fact]
+ public void LoadApplicable_SanitizesCustomParametersWhenCliArgumentsAreAllowed()
+ {
+ var manager = new PackageManagerBuilder().WithName($"Manager{Guid.NewGuid():N}").Build();
+ var package = new PackageBuilder().WithManager(manager).WithId($"Pkg{Guid.NewGuid():N}").Build();
+ var packageOptions = new InstallOptions
+ {
+ OverridesNextLevelOpts = true,
+ CustomParameters_Install = ["--keep&drop|;<>\n"],
+ };
+
+ SecureSettings.ApplyForUser(Environment.UserName, SecureSettings.ResolveKey(SecureSettings.K.AllowCLIArguments), true);
+ InstallOptionsFactory.SaveForPackage(packageOptions, package);
+
+ var resolved = InstallOptionsFactory.LoadApplicable(package);
+
+ Assert.Equal(["--keepdrop"], resolved.CustomParameters_Install);
+ }
+
+ [Fact]
+ public void SaveAndLoadForPackage_RoundTripsPersistedOptions()
+ {
+ var manager = new PackageManagerBuilder().WithName($"Manager{Guid.NewGuid():N}").Build();
+ var package = new PackageBuilder().WithManager(manager).WithId($"Pkg{Guid.NewGuid():N}").Build();
+ var expected = new InstallOptions
+ {
+ OverridesNextLevelOpts = true,
+ Architecture = "x64",
+ CustomInstallLocation = @"D:\Tools",
+ InteractiveInstallation = true,
+ SkipMinorUpdates = true,
+ };
+ expected.CustomParameters_Install.Add("--quiet");
+
+ InstallOptionsFactory.SaveForPackage(expected, package);
+
+ var actual = InstallOptionsFactory.LoadForPackage(package);
+
+ Assert.True(actual.OverridesNextLevelOpts);
+ Assert.Equal("x64", actual.Architecture);
+ Assert.Equal(@"D:\Tools", actual.CustomInstallLocation);
+ Assert.True(actual.InteractiveInstallation);
+ Assert.True(actual.SkipMinorUpdates);
+ Assert.Equal(["--quiet"], actual.CustomParameters_Install);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/NpmManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/NpmManagerTests.cs
new file mode 100644
index 0000000000..6b89681c58
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/NpmManagerTests.cs
@@ -0,0 +1,166 @@
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Managers.NpmManager;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Structs;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class NpmManagerTests
+{
+ [Fact]
+ public void ParseSearchOutputParsesJsonArrayAfterWarningPrefix()
+ {
+ var manager = new Npm();
+
+ var packages = Npm.ParseSearchOutput(
+ PackageEngineFixtureFiles.ReadAllText(@"Npm\search-array-with-warning.txt"),
+ manager.DefaultSource,
+ manager
+ );
+
+ var packageList = packages.ToArray();
+ Assert.Equal(2, packageList.Length);
+ PackageAssert.BelongsTo(packageList[0], manager, manager.DefaultSource);
+ Assert.Equal("left-pad", packageList[0].Id);
+ Assert.Equal("1.3.0", packageList[0].VersionString);
+ PackageAssert.BelongsTo(packageList[1], manager, manager.DefaultSource);
+ Assert.Equal("@types/node", packageList[1].Id);
+ Assert.Equal("24.0.0", packageList[1].VersionString);
+ }
+
+ [Fact]
+ public void ParseSearchOutputFallsBackToNdjsonAndSkipsInvalidEntries()
+ {
+ var manager = new Npm();
+
+ var packages = Npm.ParseSearchOutput(
+ PackageEngineFixtureFiles.ReadAllText(@"Npm\search-ndjson.txt"),
+ manager.DefaultSource,
+ manager
+ );
+
+ var packageList = packages.ToArray();
+ Assert.Equal(2, packageList.Length);
+ Assert.Equal("chalk", packageList[0].Id);
+ Assert.Equal("5.4.1", packageList[0].VersionString);
+ Assert.Equal("npm-check-updates", packageList[1].Id);
+ Assert.Equal("17.1.1", packageList[1].VersionString);
+ }
+
+ [Fact]
+ public void ParseAvailableUpdatesOutputCreatesPackagesWithRequestedScope()
+ {
+ var manager = new Npm();
+
+ var packages = Npm.ParseAvailableUpdatesOutput(
+ PackageEngineFixtureFiles.ReadAllText(@"Npm\outdated.json"),
+ manager.DefaultSource,
+ manager,
+ new OverridenInstallationOptions(PackageScope.Global)
+ );
+
+ var package = Assert.Single(packages);
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ Assert.Equal("npm", package.Id);
+ Assert.Equal("10.9.0", package.VersionString);
+ Assert.Equal("11.0.0", package.NewVersionString);
+ Assert.Equal(PackageScope.Global, package.OverridenOptions.Scope);
+ }
+
+ [Fact]
+ public void ParseInstalledPackagesOutputCreatesPackagesWithRequestedScope()
+ {
+ var manager = new Npm();
+
+ var packages = Npm.ParseInstalledPackagesOutput(
+ PackageEngineFixtureFiles.ReadAllText(@"Npm\installed.json"),
+ manager.DefaultSource,
+ manager,
+ new OverridenInstallationOptions(PackageScope.Local)
+ );
+
+ var package = Assert.Single(packages);
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ Assert.Equal("rimraf", package.Id);
+ Assert.Equal("6.0.1", package.VersionString);
+ Assert.Equal(PackageScope.Local, package.OverridenOptions.Scope);
+ }
+
+ [Fact]
+ public void OperationHelperBuildsInstallParametersFromOptions()
+ {
+ var manager = new Npm();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("contoso-tool")
+ .WithVersion("1.0.0")
+ .Build();
+ var options = new InstallOptions
+ {
+ Version = "2.0.0",
+ InstallationScope = PackageScope.Global,
+ PreRelease = true,
+ };
+ options.CustomParameters_Install.Add("--foreground-scripts");
+
+ var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Install);
+
+ Assert.Equal(
+ [
+ "install",
+ OperatingSystem.IsWindows() ? "'contoso-tool@2.0.0'" : "contoso-tool@2.0.0",
+ "--global",
+ "--include",
+ "dev",
+ "--foreground-scripts",
+ ],
+ parameters
+ );
+ }
+
+ [Fact]
+ public void OperationHelperLetsPackageScopeOverrideUpdateScope()
+ {
+ var manager = new Npm();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("contoso-tool")
+ .WithVersion("1.0.0")
+ .WithNewVersion("3.0.0")
+ .WithOptions(new OverridenInstallationOptions(PackageScope.Global))
+ .Build();
+ var options = new InstallOptions
+ {
+ InstallationScope = PackageScope.Local,
+ };
+ options.CustomParameters_Update.Add("--audit");
+
+ var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Update);
+
+ Assert.Equal(
+ [
+ "install",
+ OperatingSystem.IsWindows() ? "'contoso-tool@3.0.0'" : "contoso-tool@3.0.0",
+ "--global",
+ "--audit",
+ ],
+ parameters
+ );
+ }
+
+ [Fact]
+ public void OperationHelperReturnsSuccessOnlyForZeroExitCode()
+ {
+ var manager = new Npm();
+ var package = new PackageBuilder().WithManager(manager).Build();
+
+ var success = manager.OperationHelper.GetResult(package, OperationType.Install, [], 0);
+ var failure = manager.OperationHelper.GetResult(package, OperationType.Install, [], 1);
+
+ Assert.Equal(OperationVeredict.Success, success);
+ Assert.Equal(OperationVeredict.Failure, failure);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/NuGetManifestLoaderTests.cs b/src/UniGetUI.PackageEngine.Tests/NuGetManifestLoaderTests.cs
new file mode 100644
index 0000000000..383baf25d8
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/NuGetManifestLoaderTests.cs
@@ -0,0 +1,84 @@
+using UniGetUI.PackageEngine.Managers.Generic.NuGet;
+using UniGetUI.PackageEngine.Managers.Generic.NuGet.Internal;
+using UniGetUI.PackageEngine.Managers.PowerShellManager;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class NuGetManifestLoaderTests
+{
+ [Fact]
+ public void GetManifestUrlAndNuPkgUrl_UsePackageSourceAndVersion()
+ {
+ var manager = new PackageManagerBuilder().Build();
+ var source = new SourceBuilder()
+ .WithManager(manager)
+ .WithUrl("https://packages.example.test/feed")
+ .Build();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithSource(source)
+ .WithId("Contoso.Tool")
+ .WithVersion("1.2.3")
+ .Build();
+
+ Assert.Equal(
+ "https://packages.example.test/feed/Packages(Id='Contoso.Tool',Version='1.2.3')",
+ NuGetManifestLoader.GetManifestUrl(package).ToString()
+ );
+ Assert.Equal(
+ "https://packages.example.test/feed/package/Contoso.Tool/1.2.3",
+ NuGetManifestLoader.GetNuPkgUrl(package).ToString()
+ );
+ }
+
+ [Fact]
+ public void GetManifestContent_UsesCachedManifestWhenAvailable()
+ {
+ BaseNuGet.Manifests.Clear();
+ var manager = new PackageManagerBuilder().Build();
+ var package = new PackageBuilder().WithManager(manager).WithId("Contoso.Tool").Build();
+ BaseNuGet.Manifests[package.GetHash()] = "";
+
+ Assert.Equal("", NuGetManifestLoader.GetManifestContent(package));
+ }
+
+ [Fact]
+ public void GetManifestContent_FallsBackWhenTrailingZeroVersionReturnsNotFound()
+ {
+ BaseNuGet.Manifests.Clear();
+ using var server = new TestHttpServer(request =>
+ {
+ string rawUrl = request.RawUrl ?? string.Empty;
+ return rawUrl switch
+ {
+ string value when value.Contains("Version='1.0.0'") || value.Contains("Version=%271.0.0%27")
+ => (404, string.Empty, "application/xml"),
+ string value when value.Contains("Version='1.0'") || value.Contains("Version=%271.0%27")
+ => (200, "manifest", "application/xml"),
+ _ => (500, string.Empty, "text/plain"),
+ };
+ });
+ var manager = new PackageManagerBuilder().Build();
+ var source = new SourceBuilder()
+ .WithManager(manager)
+ .WithUrl(server.BaseUri.AbsoluteUri.TrimEnd('/'))
+ .Build();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithSource(source)
+ .WithId("Contoso.Tool")
+ .WithVersion("1.0.0")
+ .Build();
+
+ var manifest = NuGetManifestLoader.GetManifestContent(package);
+
+ Assert.Equal("manifest", manifest);
+ Assert.Equal(2, server.RequestPaths.Count);
+ Assert.Contains("1.0.0", server.RequestPaths[0]);
+ Assert.True(
+ server.RequestPaths[1].Contains("Version='1.0'") || server.RequestPaths[1].Contains("Version=%271.0%27")
+ );
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/PackageLoaderPipelineTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageLoaderPipelineTests.cs
new file mode 100644
index 0000000000..658ad8a0be
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/PackageLoaderPipelineTests.cs
@@ -0,0 +1,191 @@
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class PackageLoaderPipelineTests
+{
+ [Fact]
+ public async Task AddForeign_DeduplicatesByPackageHash_WhenVersionAwareIdentityIsDisabled()
+ {
+ var manager = new PackageManagerBuilder().Build();
+ var loader = new TestPackageLoader([manager], allowMultiplePackageVersions: false);
+ var packageV1 = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tool")
+ .WithVersion("1.0.0")
+ .Build();
+ var packageV2 = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tool")
+ .WithVersion("2.0.0")
+ .Build();
+
+ await loader.AddForeign(packageV1);
+ await loader.AddForeign(packageV2);
+
+ Assert.Equal(1, loader.Count());
+ Assert.True(loader.Contains(packageV1));
+ Assert.True(loader.Contains(packageV2));
+ Assert.Same(packageV1, loader.GetEquivalentPackage(packageV2));
+ Assert.Single(loader.GetEquivalentPackages(packageV1));
+ }
+
+ [Fact]
+ public async Task AddForeign_KeepsDistinctVersions_WhenVersionAwareIdentityIsEnabled()
+ {
+ var manager = new PackageManagerBuilder().Build();
+ var loader = new TestPackageLoader([manager], allowMultiplePackageVersions: true);
+ var packageV1 = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tool")
+ .WithVersion("1.0.0")
+ .Build();
+ var packageV2 = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tool")
+ .WithVersion("2.0.0")
+ .Build();
+
+ await loader.AddForeign(packageV1);
+ await loader.AddForeign(packageV2);
+
+ Assert.Equal(2, loader.Count());
+ Assert.Same(packageV1, loader.GetEquivalentPackage(packageV1));
+ Assert.Same(packageV2, loader.GetEquivalentPackage(packageV2));
+ Assert.Equal(
+ ["1.0.0", "2.0.0"],
+ loader.GetEquivalentPackages(packageV1).Select(package => package.VersionString).Order()
+ );
+ }
+
+ [Fact]
+ public async Task LookupAndRemovalApisUseLoaderIdentityAndSourceFiltering()
+ {
+ var manager = new PackageManagerBuilder()
+ .WithSources(testManager =>
+ [
+ new SourceBuilder()
+ .WithManager(testManager)
+ .WithName("stable")
+ .WithUrl("https://example.test/stable")
+ .Build(),
+ new SourceBuilder()
+ .WithManager(testManager)
+ .WithName("beta")
+ .WithUrl("https://example.test/beta")
+ .Build(),
+ ])
+ .Build();
+ var stableSource = manager.SourcesHelper.GetSources().Single(source => source.Name == "stable");
+ var betaSource = manager.SourcesHelper.GetSources().Single(source => source.Name == "beta");
+ var loader = new TestPackageLoader([manager]);
+ var uniquePackage = new PackageBuilder()
+ .WithManager(manager)
+ .WithSource(stableSource)
+ .WithId("Contoso.Unique")
+ .Build();
+ var stablePackage = new PackageBuilder()
+ .WithManager(manager)
+ .WithSource(stableSource)
+ .WithId("Contoso.Shared")
+ .Build();
+ var betaPackage = new PackageBuilder()
+ .WithManager(manager)
+ .WithSource(betaSource)
+ .WithId("Contoso.Shared")
+ .Build();
+ var recorder = new LoaderEventRecorder(loader);
+
+ await loader.AddForeign(uniquePackage);
+ await loader.AddForeign(stablePackage);
+ await loader.AddForeign(betaPackage);
+ loader.Remove(betaPackage);
+
+ Assert.True(loader.Contains(stablePackage));
+ Assert.False(loader.Contains(betaPackage));
+ Assert.Same(uniquePackage, loader.GetPackageForId("Contoso.Unique"));
+ Assert.Same(stablePackage, loader.GetPackageForId("Contoso.Shared", "stable"));
+ Assert.Null(loader.GetPackageForId("Contoso.Shared", "beta"));
+ Assert.Single(loader.GetEquivalentPackages(stablePackage));
+ Assert.Null(loader.GetEquivalentPackage(betaPackage));
+ Assert.Contains(recorder.RemovedPackages, package => package.Id == "Contoso.Shared");
+ }
+
+ [Fact]
+ public async Task ClearPackagesAndStopLoading_EmitExpectedEvents()
+ {
+ var manager = new PackageManagerBuilder().Build();
+ var loader = new TestPackageLoader([manager]);
+ var recorder = new LoaderEventRecorder(loader);
+ var package = new PackageBuilder().WithManager(manager).WithId("Contoso.Tool").Build();
+
+ await loader.AddForeign(package);
+ loader.StopLoading(emitFinishSignal: false);
+ loader.ClearPackages();
+
+ Assert.Equal(0, loader.Count());
+ Assert.Equal(1, recorder.FinishedLoadingCount);
+ Assert.Equal(2, recorder.Changes.Count);
+ Assert.False(recorder.Changes[1].ProceduralChange);
+ Assert.Empty(recorder.Changes[1].AddedPackages);
+ Assert.Empty(recorder.Changes[1].RemovedPackages);
+ }
+
+ [Fact]
+ public async Task ReloadPackages_ClearsExistingPackages_FiltersInvalidOnes_AndRaisesLifecycleEvents()
+ {
+ var manager = new PackageManagerBuilder()
+ .WithInstalledPackages(testManager =>
+ [
+ new PackageBuilder()
+ .WithManager(testManager)
+ .WithId("Contoso.Accepted")
+ .WithVersion("1.0.0")
+ .Build(),
+ new PackageBuilder()
+ .WithManager(testManager)
+ .WithId("Contoso.Accepted")
+ .WithVersion("1.0.0")
+ .Build(),
+ new PackageBuilder()
+ .WithManager(testManager)
+ .WithId("Contoso.Rejected")
+ .WithVersion("1.0.0")
+ .Build(),
+ ])
+ .Build();
+ var loader = new TestPackageLoader(
+ [manager],
+ loadPackages: testManager => testManager.GetInstalledPackages(),
+ isPackageValid: package => Task.FromResult(package.Id != "Contoso.Rejected")
+ );
+ var stalePackage = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Stale")
+ .WithVersion("0.9.0")
+ .Build();
+ var recorder = new LoaderEventRecorder(loader);
+
+ await loader.AddForeign(stalePackage);
+ await loader.ReloadPackages();
+
+ var changeEvents = recorder.Changes.Skip(1).ToArray();
+
+ Assert.True(recorder.StartedLoading);
+ Assert.True(recorder.FinishedLoading);
+ Assert.Equal(1, recorder.StartedLoadingCount);
+ Assert.Equal(1, recorder.FinishedLoadingCount);
+ Assert.True(loader.IsLoaded);
+ Assert.False(loader.IsLoading);
+ Assert.Equal(["Contoso.Accepted"], loader.Packages.Select(package => package.Id).Distinct());
+ Assert.Single(loader.Packages);
+ Assert.Equal(2, changeEvents.Length);
+ Assert.False(changeEvents[0].ProceduralChange);
+ Assert.Empty(changeEvents[0].AddedPackages);
+ Assert.True(changeEvents[1].ProceduralChange);
+ Assert.Equal(["Contoso.Accepted"], changeEvents[1].AddedPackages.Select(package => package.Id).Distinct());
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs
new file mode 100644
index 0000000000..b9d8a7a249
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs
@@ -0,0 +1,334 @@
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.Core.SettingsEngine.SecureSettings;
+using UniGetUI.Core.Tools;
+using UniGetUI.PackageEngine.PackageClasses;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class PackageManagerTests : IDisposable
+{
+ private readonly string _testRoot;
+
+ public PackageManagerTests()
+ {
+ _testRoot = Path.Combine(
+ AppContext.BaseDirectory,
+ nameof(PackageManagerTests),
+ Guid.NewGuid().ToString("N")
+ );
+ var secureSettingsRoot = Path.Combine(_testRoot, "SecureSettings");
+
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ SecureSettings.TEST_SecureSettingsRootOverride = secureSettingsRoot;
+
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Directory.CreateDirectory(secureSettingsRoot);
+
+ Settings.ResetSettings();
+ Settings.SetDictionary(Settings.K.DisabledManagers, new Dictionary());
+ Settings.SetDictionary(Settings.K.ManagerPaths, new Dictionary());
+ SecureSettings.ApplyForUser(
+ Environment.UserName,
+ SecureSettings.ResolveKey(SecureSettings.K.AllowCustomManagerPaths),
+ false
+ );
+ }
+
+ public void Dispose()
+ {
+ Settings.SetDictionary(Settings.K.DisabledManagers, new Dictionary());
+ Settings.SetDictionary(Settings.K.ManagerPaths, new Dictionary());
+ SecureSettings.ApplyForUser(
+ Environment.UserName,
+ SecureSettings.ResolveKey(SecureSettings.K.AllowCustomManagerPaths),
+ false
+ );
+
+ CoreData.TEST_DataDirectoryOverride = null;
+ SecureSettings.TEST_SecureSettingsRootOverride = null;
+
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, true);
+ }
+ }
+
+ [Fact]
+ public void InitializeDisabledManagerSkipsLoadingAndStaysNotReady()
+ {
+ var manager = CreateManager();
+ Settings.SetDictionaryItem(Settings.K.DisabledManagers, manager.Name, true);
+
+ manager.Initialize();
+
+ Assert.False(manager.IsEnabled());
+ Assert.False(manager.IsReady());
+ Assert.False(manager.Status.Found);
+ Assert.Equal(string.Empty, manager.Status.ExecutablePath);
+ Assert.Equal(
+ CoreTools.Translate("{0} is disabled", manager.DisplayName),
+ manager.Status.Version
+ );
+ }
+
+ [Fact]
+ public void InitializeEnabledManagerWithoutExecutableStaysNotReady()
+ {
+ var manager = CreateManager();
+ manager.ExecutableFound = false;
+ manager.ExecutablePath = CreateExecutable("missing-manager.exe");
+
+ manager.Initialize();
+
+ Assert.True(manager.IsEnabled());
+ Assert.False(manager.IsReady());
+ Assert.False(manager.Status.Found);
+ Assert.Equal(manager.ExecutablePath, manager.Status.ExecutablePath);
+ Assert.Equal(
+ CoreTools.Translate("{pm} was not found!").Replace("{pm}", manager.DisplayName).Trim('!'),
+ manager.Status.Version
+ );
+ }
+
+ [Fact]
+ public void InitializeEnabledManagerWithExecutableBecomesReady()
+ {
+ var manager = CreateManager();
+ manager.ExecutablePath = CreateExecutable("ready-manager.exe");
+ manager.LoadedVersion = "9.9.9-test";
+
+ manager.Initialize();
+
+ Assert.True(manager.IsEnabled());
+ Assert.True(manager.IsReady());
+ Assert.True(manager.Status.Found);
+ Assert.Equal(manager.ExecutablePath, manager.Status.ExecutablePath);
+ Assert.Equal("9.9.9-test", manager.Status.Version);
+ }
+
+ [Fact]
+ public void IsReadyTracksEnablementChangesAfterInitialization()
+ {
+ var manager = CreateManager();
+ manager.ExecutablePath = CreateExecutable("toggle-manager.exe");
+
+ Assert.True(manager.IsEnabled());
+ Assert.False(manager.IsReady());
+
+ manager.Initialize();
+
+ Assert.True(manager.IsReady());
+
+ Settings.SetDictionaryItem(Settings.K.DisabledManagers, manager.Name, true);
+
+ Assert.False(manager.IsEnabled());
+ Assert.False(manager.IsReady());
+
+ Settings.SetDictionaryItem(Settings.K.DisabledManagers, manager.Name, false);
+
+ Assert.True(manager.IsEnabled());
+ Assert.True(manager.IsReady());
+ }
+
+ [Fact]
+ public void GetExecutableFileReturnsFalseWhenNoCandidatesExist()
+ {
+ var manager = CreateManager();
+ manager.SetCandidateExecutableFiles();
+
+ var executable = manager.GetExecutableFile();
+
+ Assert.False(executable.Item1);
+ Assert.Equal(string.Empty, executable.Item2);
+ }
+
+ [Fact]
+ public void GetExecutableFileIgnoresSavedPathWhenCustomPathsAreDisabled()
+ {
+ var manager = CreateManager();
+ var first = CreateExecutable("candidate-a.exe");
+ var second = CreateExecutable("candidate-b.exe");
+ manager.SetCandidateExecutableFiles(first, second);
+ Settings.SetDictionaryItem(Settings.K.ManagerPaths, manager.Name, second);
+
+ var executable = manager.GetExecutableFile();
+
+ Assert.True(executable.Item1);
+ Assert.Equal(first, executable.Item2);
+ }
+
+ [Fact]
+ public void GetExecutableFileUsesSavedCandidateWhenCustomPathsAreEnabled()
+ {
+ var manager = CreateManager();
+ var first = CreateExecutable("enabled-a.exe");
+ var second = CreateExecutable("enabled-b.exe");
+ manager.SetCandidateExecutableFiles(first, second);
+ EnableCustomManagerPaths();
+ Settings.SetDictionaryItem(Settings.K.ManagerPaths, manager.Name, second);
+
+ var executable = manager.GetExecutableFile();
+
+ Assert.True(executable.Item1);
+ Assert.Equal(second, executable.Item2);
+ }
+
+ [Fact]
+ public void GetExecutableFileFallsBackWhenSavedPathDoesNotExist()
+ {
+ var manager = CreateManager();
+ var first = CreateExecutable("fallback-a.exe");
+ var second = CreateExecutable("fallback-b.exe");
+ manager.SetCandidateExecutableFiles(first, second);
+ EnableCustomManagerPaths();
+ Settings.SetDictionaryItem(
+ Settings.K.ManagerPaths,
+ manager.Name,
+ Path.Combine(_testRoot, "missing.exe")
+ );
+
+ var executable = manager.GetExecutableFile();
+
+ Assert.True(executable.Item1);
+ Assert.Equal(first, executable.Item2);
+ }
+
+ [Fact]
+ public void GetExecutableFileFallsBackWhenSavedPathIsNotACandidate()
+ {
+ var manager = CreateManager();
+ var first = CreateExecutable("outside-a.exe");
+ var second = CreateExecutable("outside-b.exe");
+ var outsideCandidateList = CreateExecutable("outside-c.exe");
+ manager.SetCandidateExecutableFiles(first, second);
+ EnableCustomManagerPaths();
+ Settings.SetDictionaryItem(
+ Settings.K.ManagerPaths,
+ manager.Name,
+ outsideCandidateList
+ );
+
+ var executable = manager.GetExecutableFile();
+
+ Assert.True(executable.Item1);
+ Assert.Equal(first, executable.Item2);
+ }
+
+ [Fact]
+ public void FindPackagesReturnsEmptyWhenManagerIsNotReady()
+ {
+ var manager = CreateManager();
+
+ var packages = manager.FindPackages("tool");
+
+ Assert.Empty(packages);
+ Assert.Null(manager.LastQuery);
+ Assert.Equal(0, manager.AttemptFastRepairCalls);
+ }
+
+ [Fact]
+ public void FindPackagesRetriesOnceAfterFailure()
+ {
+ var manager = CreateReadyManager();
+ var attempts = 0;
+ manager.SetFindPackages(query =>
+ {
+ attempts++;
+ return attempts == 1
+ ? throw new InvalidOperationException("search failed")
+ : [CreatePackage(manager, "Contoso.Search", "Contoso Search")];
+ });
+
+ var packages = manager.FindPackages("Contoso");
+
+ var package = Assert.Single(packages);
+ Assert.Equal("Contoso.Search", package.Id);
+ Assert.Equal("Contoso", manager.LastQuery);
+ Assert.Equal(1, manager.AttemptFastRepairCalls);
+ }
+
+ [Fact]
+ public void GetAvailableUpdatesRetriesOnceAndRefreshesIndexesPerAttempt()
+ {
+ var manager = CreateReadyManager();
+ var attempts = 0;
+ manager.SetAvailableUpdates(() =>
+ {
+ attempts++;
+ return attempts == 1
+ ? throw new InvalidOperationException("updates failed")
+ : [CreatePackage(manager, "Contoso.Update", "Contoso Update", "1.0.0", "2.0.0")];
+ });
+
+ var packages = manager.GetAvailableUpdates();
+
+ var package = Assert.Single(packages);
+ Assert.Equal("Contoso.Update", package.Id);
+ Assert.Equal(1, manager.AttemptFastRepairCalls);
+ Assert.Equal(2, manager.RefreshPackageIndexesCalls);
+ }
+
+ [Fact]
+ public void GetInstalledPackagesReturnsEmptyAfterSecondFailure()
+ {
+ var manager = CreateReadyManager();
+ manager.SetInstalledPackages(() => throw new InvalidOperationException("installed failed"));
+
+ var packages = manager.GetInstalledPackages();
+
+ Assert.Empty(packages);
+ Assert.Equal(1, manager.AttemptFastRepairCalls);
+ }
+
+ private static TestPackageManager CreateManager()
+ {
+ return new PackageManagerBuilder()
+ .WithName($"TestManager-{Guid.NewGuid():N}")
+ .WithDisplayName("Test Manager")
+ .Build(initialize: false);
+ }
+
+ private TestPackageManager CreateReadyManager()
+ {
+ var manager = CreateManager();
+ manager.ExecutablePath = CreateExecutable($"{manager.Name}.exe");
+ manager.Initialize();
+ Assert.True(manager.IsReady());
+ return manager;
+ }
+
+ private static void EnableCustomManagerPaths()
+ {
+ Assert.Equal(
+ 0,
+ SecureSettings.ApplyForUser(
+ Environment.UserName,
+ SecureSettings.ResolveKey(SecureSettings.K.AllowCustomManagerPaths),
+ true
+ )
+ );
+ }
+
+ private string CreateExecutable(string fileName)
+ {
+ var path = Path.Combine(_testRoot, "Executables", fileName);
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ File.WriteAllText(path, string.Empty);
+ return path;
+ }
+
+ private static Package CreatePackage(
+ TestPackageManager manager,
+ string id,
+ string name,
+ string version = "1.0.0",
+ string? newVersion = null
+ )
+ {
+ var builder = new PackageBuilder().WithManager(manager).WithId(id).WithName(name).WithVersion(version);
+ return newVersion is null ? builder.Build() : builder.WithNewVersion(newVersion).Build();
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs
new file mode 100644
index 0000000000..a2ca9ec9c1
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs
@@ -0,0 +1,356 @@
+using System.Diagnostics;
+using System.Reflection;
+using UniGetUI.Core.Tools;
+using UniGetUI.Interface.Enums;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.Operations;
+using UniGetUI.PackageEngine.PackageLoader;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Structs;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageOperations;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+[CollectionDefinition(nameof(OperationOrchestrationTestCollection), DisableParallelization = true)]
+public sealed class OperationOrchestrationTestCollection;
+
+[Collection(nameof(OperationOrchestrationTestCollection))]
+public sealed class PackageOperationsTests
+{
+ [Fact]
+ public void RetryModesMutateInstallOptionsAndMetadata()
+ {
+ var package = CreatePackage();
+ var options = new InstallOptions();
+ var operation = new InspectableInstallPackageOperation(package, options);
+
+ operation.Retry(AbstractOperation.RetryMode.Retry_AsAdmin);
+ operation.Retry(AbstractOperation.RetryMode.Retry_Interactive);
+ operation.Retry(AbstractOperation.RetryMode.Retry_SkipIntegrity);
+
+ Assert.True(options.RunAsAdministrator);
+ Assert.True(options.InteractiveInstallation);
+ Assert.True(options.SkipHashCheck);
+ Assert.Contains("Retried package operation", operation.Metadata.OperationInformation);
+ Assert.Contains(package.Id, operation.Metadata.OperationInformation);
+ Assert.Throws(() => operation.Retry("InvalidRetryMode"));
+ }
+
+ [Fact]
+ public void InstallOperationBuildsPrerequisitesKillListAndPreCommand()
+ {
+ var package = CreatePackage();
+ var options = new InstallOptions
+ {
+ PreInstallCommand = "echo before install",
+ AbortOnPreInstallFail = false,
+ };
+ options.KillBeforeOperation.Add("proc-one");
+ options.KillBeforeOperation.Add("proc-two");
+ using var prerequisite = new StubOperation();
+ using var operation = new InspectableInstallPackageOperation(package, options, req: prerequisite);
+
+ var preOperations = GetInnerOperations(operation, "PreOperations");
+
+ Assert.Collection(
+ preOperations,
+ inner =>
+ {
+ Assert.Same(prerequisite, inner.Operation);
+ Assert.True(inner.MustSucceed);
+ },
+ inner =>
+ {
+ Assert.IsType(inner.Operation);
+ Assert.False(inner.MustSucceed);
+ },
+ inner =>
+ {
+ Assert.IsType(inner.Operation);
+ Assert.False(inner.MustSucceed);
+ },
+ inner =>
+ {
+ var preCommand = Assert.IsType(inner.Operation);
+ Assert.False(inner.MustSucceed);
+ Assert.Contains("echo before install", preCommand.Metadata.Status);
+ }
+ );
+ }
+
+ [Fact]
+ public async Task UpdateOperationBuildsPostOperationsForCommandAndPreviousVersions()
+ {
+ var manager = CreateManager();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tool")
+ .WithVersion("1.0.0")
+ .WithNewVersion("3.0.0")
+ .Build();
+ var olderInstalledVersion = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tool")
+ .WithVersion("2.0.0")
+ .Build();
+ var newerInstalledVersion = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tool")
+ .WithVersion("3.1.0")
+ .Build();
+ InitializeLoaders();
+ await InstalledPackagesLoader.Instance.AddForeign(olderInstalledVersion);
+ await InstalledPackagesLoader.Instance.AddForeign(newerInstalledVersion);
+ var options = new InstallOptions
+ {
+ PostUpdateCommand = "echo after update",
+ UninstallPreviousVersionsOnUpdate = true,
+ };
+
+ using var operation = new UpdatePackageOperation(package, options);
+ var postOperations = GetInnerOperations(operation, "PostOperations");
+
+ Assert.Collection(
+ postOperations,
+ inner =>
+ {
+ var postCommand = Assert.IsType(inner.Operation);
+ Assert.False(inner.MustSucceed);
+ Assert.Contains("echo after update", postCommand.Metadata.Status);
+ },
+ inner =>
+ {
+ var uninstall = Assert.IsType(inner.Operation);
+ Assert.False(inner.MustSucceed);
+ Assert.Equal("2.0.0", uninstall.Package.VersionString);
+ }
+ );
+ Assert.Contains("1.0.0 -> 3.0.0", operation.Metadata.OperationInformation);
+ }
+
+ [Fact]
+ public void UninstallOperationBuildsPreAndPostCommandsForUninstallPath()
+ {
+ var package = CreatePackage();
+ var options = new InstallOptions
+ {
+ PreUninstallCommand = "echo before uninstall",
+ AbortOnPreUninstallFail = false,
+ PostUninstallCommand = "echo after uninstall",
+ };
+ using var operation = new UninstallPackageOperation(package, options);
+
+ var preOperations = GetInnerOperations(operation, "PreOperations");
+ var postOperations = GetInnerOperations(operation, "PostOperations");
+
+ var preOperation = Assert.Single(preOperations);
+ var preCommand = Assert.IsType(preOperation.Operation);
+ Assert.False(preOperation.MustSucceed);
+ Assert.Contains("echo before uninstall", preCommand.Metadata.Status);
+
+ var postOperation = Assert.Single(postOperations);
+ var postCommand = Assert.IsType(postOperation.Operation);
+ Assert.False(postOperation.MustSucceed);
+ Assert.Contains("echo after uninstall", postCommand.Metadata.Status);
+ }
+
+ [Fact]
+ public void InstallOperationPrepareProcessStartInfoUsesManagerCommandLineAndSetsBadges()
+ {
+ var manager = new PackageManagerBuilder()
+ .ConfigureManager(manager =>
+ {
+ manager.ExecutablePath = "C:\\tools\\pkgmgr.exe";
+ manager.ExecutableArguments = "--cli";
+ })
+ .ConfigureOperation(helper =>
+ helper.ParametersFactory = (package, _, operation) =>
+ [
+ operation.ToString().ToLowerInvariant(),
+ package.Id,
+ ])
+ .Build();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithOptions(new OverridenInstallationOptions(scope: PackageScope.Machine))
+ .Build();
+ var options = new InstallOptions
+ {
+ InstallationScope = PackageScope.User,
+ InteractiveInstallation = true,
+ SkipHashCheck = true,
+ };
+ using var operation = new InspectableInstallPackageOperation(package, options);
+ AbstractOperation.BadgeCollection? badges = null;
+ operation.BadgesChanged += (_, updatedBadges) => badges = updatedBadges;
+
+ var startInfo = operation.PrepareProcessStartInfoForTests();
+
+ Assert.Equal("C:\\tools\\pkgmgr.exe", startInfo.FileName);
+ Assert.Equal("--cli install Contoso.Test", startInfo.Arguments.Trim());
+ Assert.Equal(PackageTag.OnQueue, package.Tag);
+ Assert.NotNull(badges);
+ Assert.Equal(CoreTools.IsAdministrator(), badges!.AsAdministrator);
+ Assert.True(badges.Interactive);
+ Assert.True(badges.SkipHashCheck);
+ Assert.Equal(PackageScope.Machine, badges.Scope);
+ }
+
+ [Fact]
+ public async Task InstallOperationSuccessfulRunSetsPackageTagAndAddsInstalledCopy()
+ {
+ var package = CreatePackage();
+ InitializeLoaders();
+ using var operation = new SimulatedInstallPackageOperation(
+ package,
+ new InstallOptions(),
+ OperationVeredict.Success
+ );
+
+ await operation.MainThread();
+ await WaitForAsync(() => InstalledPackagesLoader.Instance.GetEquivalentPackage(package) is not null);
+
+ Assert.Equal(PackageTag.AlreadyInstalled, package.Tag);
+ Assert.NotNull(InstalledPackagesLoader.Instance.GetEquivalentPackage(package));
+ }
+
+ private static IReadOnlyList GetInnerOperations(
+ AbstractOperation operation,
+ string fieldName
+ )
+ {
+ var field = typeof(AbstractOperation).GetField(
+ fieldName,
+ BindingFlags.Instance | BindingFlags.NonPublic
+ );
+
+ return Assert.IsAssignableFrom>(
+ field?.GetValue(operation)
+ );
+ }
+
+ private static IPackage CreatePackage()
+ {
+ var manager = CreateManager();
+ return new PackageBuilder().WithManager(manager).Build();
+ }
+
+ private static IPackageManager CreateManager()
+ {
+ return new PackageManagerBuilder()
+ .ConfigureManager(manager =>
+ {
+ manager.ExecutablePath = "C:\\test-tools\\manager.exe";
+ manager.ExecutableArguments = "--test";
+ })
+ .ConfigureOperation(helper =>
+ helper.ParametersFactory = (package, _, operation) =>
+ [
+ operation.ToString().ToLowerInvariant(),
+ package.Id,
+ ])
+ .Build();
+ }
+
+ private static void InitializeLoaders()
+ {
+ _ = new DiscoverablePackagesLoader([]);
+ _ = new UpgradablePackagesLoader([]);
+ _ = new InstalledPackagesLoader([]);
+ }
+
+ private static async Task WaitForAsync(Func condition)
+ {
+ for (var attempt = 0; attempt < 20; attempt++)
+ {
+ if (condition())
+ return;
+
+ await Task.Delay(25);
+ }
+ }
+
+ private class InspectableInstallPackageOperation : InstallPackageOperation
+ {
+ public InspectableInstallPackageOperation(
+ IPackage package,
+ InstallOptions options,
+ bool ignoreParallelInstalls = true,
+ AbstractOperation? req = null
+ )
+ : base(package, options, ignoreParallelInstalls, req) { }
+
+ public ProcessStartInfo PrepareProcessStartInfoForTests()
+ {
+ InitializeProcessStartInfoDefaults();
+ PrepareProcessStartInfo();
+ return process.StartInfo;
+ }
+
+ private void InitializeProcessStartInfoDefaults()
+ {
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardInput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.StandardErrorEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.StandardInputEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.WorkingDirectory = Environment.GetFolderPath(
+ Environment.SpecialFolder.UserProfile
+ );
+ process.StartInfo.FileName = "lol";
+ process.StartInfo.Arguments = "lol";
+ }
+ }
+
+ private sealed class SimulatedInstallPackageOperation : InspectableInstallPackageOperation
+ {
+ private readonly OperationVeredict _veredict;
+
+ public SimulatedInstallPackageOperation(
+ IPackage package,
+ InstallOptions options,
+ OperationVeredict veredict
+ )
+ : base(package, options)
+ {
+ _veredict = veredict;
+ }
+
+ protected override Task PerformOperation()
+ {
+ return Task.FromResult(_veredict);
+ }
+ }
+
+ private sealed class StubOperation : AbstractOperation
+ {
+ public StubOperation()
+ : base(queue_enabled: false)
+ {
+ Metadata.Status = "Stub status";
+ Metadata.Title = "Stub title";
+ Metadata.OperationInformation = "Stub info";
+ Metadata.SuccessTitle = "Stub success";
+ Metadata.SuccessMessage = "Stub success";
+ Metadata.FailureTitle = "Stub failure";
+ Metadata.FailureMessage = "Stub failure";
+ }
+
+ protected override void ApplyRetryAction(string retryMode) { }
+
+ protected override Task PerformOperation()
+ {
+ return Task.FromResult(OperationVeredict.Success);
+ }
+
+ public override Task GetOperationIcon()
+ {
+ return Task.FromResult(new Uri("about:blank"));
+ }
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/PipManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PipManagerTests.cs
new file mode 100644
index 0000000000..67aa1d1398
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/PipManagerTests.cs
@@ -0,0 +1,222 @@
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Managers.PipManager;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Structs;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+[CollectionDefinition("Pip manager tests", DisableParallelization = true)]
+public sealed class PipManagerTestCollection
+{
+ public const string Name = "Pip manager tests";
+}
+
+[Collection(PipManagerTestCollection.Name)]
+public sealed class PipManagerTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ AppContext.BaseDirectory,
+ nameof(PipManagerTests),
+ Guid.NewGuid().ToString("N")
+ );
+
+ public PipManagerTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Settings.ResetSettings();
+ Settings.Set(Settings.K.EnableProxy, false);
+ Settings.Set(Settings.K.EnableProxyAuth, false);
+ Settings.SetValue(Settings.K.ProxyURL, "");
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void SearchHelpersParseSimpleIndexAndRankPrefixMatchesFirst()
+ {
+ var names = Pip.ParseSimpleIndexProjectNames(
+ PackageEngineFixtureFiles.ReadAllText(Path.Combine("Pip", "simple-index.json"))
+ );
+
+ var matches = Pip.SelectSearchMatches("req", names);
+
+ Assert.Equal(
+ ["req", "requests", "requestium", "requests-cache", "django-reqtools"],
+ matches
+ );
+ }
+
+ [Fact]
+ public void ParseAvailableUpdatesBuildsPackagesFromFixture()
+ {
+ var manager = new Pip();
+
+ var packages = Pip.ParseAvailableUpdates(
+ File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Pip", "outdated-list.txt"))),
+ manager.DefaultSource,
+ manager
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "Requests", "requests", "2.31.0", "2.32.3");
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ },
+ package =>
+ {
+ PackageAssert.Matches(
+ package,
+ "Django Reqtools",
+ "django-reqtools",
+ "1.0.0",
+ "1.1.0"
+ );
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseInstalledPackagesBuildsPackagesFromFixture()
+ {
+ var manager = new Pip();
+
+ var packages = Pip.ParseInstalledPackages(
+ File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Pip", "installed-list.txt"))),
+ manager.DefaultSource,
+ manager
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "Requests", "requests", "2.31.0");
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ },
+ package =>
+ {
+ PackageAssert.Matches(package, "Django Reqtools", "django-reqtools", "1.0.0");
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ }
+ );
+ }
+
+ [Fact]
+ public void OperationHelperBuildsInstallAndUninstallParameters()
+ {
+ Settings.Set(Settings.K.EnableProxy, true);
+ Settings.Set(Settings.K.EnableProxyAuth, false);
+ Settings.SetValue(Settings.K.ProxyURL, "http://proxy.example.test:3128/");
+ var manager = new Pip();
+ var overridenOptions = new OverridenInstallationOptions();
+ overridenOptions.Pip_BreakSystemPackages = true;
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("requests")
+ .WithOptions(overridenOptions)
+ .Build();
+ var installOptions = new InstallOptions
+ {
+ Version = "2.31.0",
+ InstallationScope = PackageScope.User,
+ PreRelease = true,
+ CustomParameters_Install = ["--quiet"],
+ };
+ var uninstallOptions = new InstallOptions
+ {
+ CustomParameters_Uninstall = ["--verbose"],
+ };
+
+ var installParameters = manager.OperationHelper.GetParameters(
+ package,
+ installOptions,
+ OperationType.Install
+ );
+ var uninstallParameters = manager.OperationHelper.GetParameters(
+ package,
+ uninstallOptions,
+ OperationType.Uninstall
+ );
+
+ Assert.Equal(
+ [
+ "install",
+ "requests==2.31.0",
+ "--no-input",
+ "--no-color",
+ "--no-cache",
+ "--pre",
+ "--user",
+ "--break-system-packages",
+ "--proxy http://proxy.example.test:3128/",
+ "--quiet",
+ ],
+ installParameters
+ );
+ Assert.Equal(
+ [
+ "uninstall",
+ "requests",
+ "--no-input",
+ "--no-color",
+ "--no-cache",
+ "--yes",
+ "--break-system-packages",
+ "--proxy http://proxy.example.test:3128/",
+ "--verbose",
+ ],
+ uninstallParameters
+ );
+ }
+
+ [Fact]
+ public void OperationHelperUsesAutoRetryMutationsForKnownPipFailures()
+ {
+ var manager = new Pip();
+ var breakSystemPackage = new PackageBuilder().WithManager(manager).WithId("requests").Build();
+ var userScopedPackage = new PackageBuilder().WithManager(manager).WithId("requests").Build();
+
+ var externallyManagedResult = manager.OperationHelper.GetResult(
+ breakSystemPackage,
+ OperationType.Install,
+ ["error: externally-managed-environment"],
+ 1
+ );
+ var userScopeResult = manager.OperationHelper.GetResult(
+ userScopedPackage,
+ OperationType.Install,
+ ["hint: try again with --user"],
+ 1
+ );
+ var failureResult = manager.OperationHelper.GetResult(
+ new PackageBuilder().WithManager(manager).WithId("requests").Build(),
+ OperationType.Install,
+ ["boom"],
+ 1
+ );
+
+ Assert.Equal(OperationVeredict.AutoRetry, externallyManagedResult);
+ Assert.True(breakSystemPackage.OverridenOptions.Pip_BreakSystemPackages);
+ Assert.Equal(OperationVeredict.AutoRetry, userScopeResult);
+ Assert.Equal(PackageScope.User, userScopedPackage.OverridenOptions.Scope);
+ Assert.Equal(OperationVeredict.Failure, failureResult);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs
new file mode 100644
index 0000000000..351e014300
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs
@@ -0,0 +1,59 @@
+#if WINDOWS
+using UniGetUI.PackageEngine.Managers.PowerShellManager;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class PowerShellManagerTests
+{
+ [Fact]
+ public void ParseInstalledPackages_BuildsPackagesFromModuleTable()
+ {
+ var manager = new PowerShell();
+ var packages = PowerShell.ParseInstalledPackages(
+ [
+ "Version Name Repository Description",
+ "------- ---- ---------- -----------",
+ "5.5.0 Pester PSGallery Test framework",
+ "2.2.5 PSReadLine PSGallery Command line editing",
+ ],
+ manager
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ Assert.Equal("Pester", package.Id);
+ Assert.Equal("5.5.0", package.VersionString);
+ Assert.Equal("PSGallery", package.Source.Name);
+ },
+ package =>
+ {
+ Assert.Equal("PSReadLine", package.Id);
+ Assert.Equal("2.2.5", package.VersionString);
+ Assert.Equal("PSGallery", package.Source.Name);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseInstalledPackages_SkipsMalformedLines()
+ {
+ var manager = new PowerShell();
+
+ var package = Assert.Single(
+ PowerShell.ParseInstalledPackages(
+ [
+ "Version Name Repository Description",
+ "------- ---- ---------- -----------",
+ "not-enough-columns",
+ "5.5.0 Pester PSGallery Test framework",
+ ],
+ manager
+ )
+ );
+
+ Assert.Equal("Pester", package.Id);
+ }
+}
+#endif
diff --git a/src/UniGetUI.PackageEngine.Tests/ScoopManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/ScoopManagerTests.cs
new file mode 100644
index 0000000000..309693608b
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/ScoopManagerTests.cs
@@ -0,0 +1,328 @@
+#if WINDOWS
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.Managers.ScoopManager;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Structs;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+using Architecture = UniGetUI.PackageEngine.Enums.Architecture;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+[CollectionDefinition("Scoop manager tests", DisableParallelization = true)]
+public sealed class ScoopManagerTestCollection
+{
+ public const string Name = "Scoop manager tests";
+}
+
+[Collection(ScoopManagerTestCollection.Name)]
+public sealed class ScoopManagerTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ AppContext.BaseDirectory,
+ nameof(ScoopManagerTests),
+ Guid.NewGuid().ToString("N")
+ );
+
+ public ScoopManagerTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Settings.ResetSettings();
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseSearchOutputBuildsPackagesFromBucketSections()
+ {
+ var manager = CreateManagerWithKnownSources("main", "versions");
+
+ var packages = manager.ParseSearchOutput(ReadFixtureLines(@"Scoop\search-output.txt"));
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "7zip", "7zip", "24.09");
+ PackageAssert.BelongsTo(package, manager, manager.SourcesHelper.Factory.GetSourceOrDefault("main"));
+ },
+ package =>
+ {
+ PackageAssert.Matches(package, "Python310", "python310", "3.10.11");
+ PackageAssert.BelongsTo(
+ package,
+ manager,
+ manager.SourcesHelper.Factory.GetSourceOrDefault("versions")
+ );
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseInstalledPackagesBuildsPackagesAndScopesFromFixture()
+ {
+ var manager = CreateManagerWithKnownSources("main", "versions");
+
+ var packages = manager.ParseInstalledPackages(ReadFixtureLines(@"Scoop\list-output.txt"));
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "Git", "git", "2.47.1");
+ PackageAssert.BelongsTo(package, manager, manager.SourcesHelper.Factory.GetSourceOrDefault("main"));
+ Assert.Equal(PackageScope.User, package.OverridenOptions.Scope);
+ },
+ package =>
+ {
+ PackageAssert.Matches(package, "Pwsh", "pwsh", "7.4.6");
+ PackageAssert.BelongsTo(
+ package,
+ manager,
+ manager.SourcesHelper.Factory.GetSourceOrDefault("versions")
+ );
+ Assert.Equal(PackageScope.Global, package.OverridenOptions.Scope);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseAvailableUpdatesPreservesInstalledSourceAndScope()
+ {
+ var manager = CreateManagerWithKnownSources("main", "versions");
+ var installedPackages = manager.ParseInstalledPackages(ReadFixtureLines(@"Scoop\list-output.txt"));
+
+ var packages = manager.ParseAvailableUpdates(
+ ReadFixtureLines(@"Scoop\status-output.txt"),
+ installedPackages
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "Git", "git", "2.47.1", "2.48.1");
+ PackageAssert.BelongsTo(package, manager, manager.SourcesHelper.Factory.GetSourceOrDefault("main"));
+ Assert.Equal(PackageScope.User, package.OverridenOptions.Scope);
+ },
+ package =>
+ {
+ PackageAssert.Matches(package, "Pwsh", "pwsh", "7.4.6", "7.5.0");
+ PackageAssert.BelongsTo(
+ package,
+ manager,
+ manager.SourcesHelper.Factory.GetSourceOrDefault("versions")
+ );
+ Assert.Equal(PackageScope.Global, package.OverridenOptions.Scope);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseSourcesNormalizesGitUrlsAndLocalBuckets()
+ {
+ var manager = new Scoop();
+ var helper = Assert.IsType(manager.SourcesHelper);
+
+ var sources = helper.ParseSources(ReadFixtureLines(@"Scoop\bucket-list-output.txt"));
+
+ Assert.Collection(
+ sources,
+ source =>
+ {
+ Assert.Equal("main", source.Name);
+ Assert.Equal(new Uri("https://github.com/ScoopInstaller/Main"), source.Url);
+ Assert.Equal(1234, source.PackageCount);
+ Assert.Equal("2024-02-01 12:34:56", source.UpdateDate);
+ },
+ source =>
+ {
+ Assert.Equal("extras", source.Name);
+ Assert.Equal(
+ new Uri(
+ Path.Join(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ "scoop",
+ "buckets",
+ "extras"
+ )
+ ),
+ source.Url
+ );
+ Assert.Equal(321, source.PackageCount);
+ Assert.Equal("2024-02-02 09:08:07", source.UpdateDate);
+ }
+ );
+ }
+
+ [Fact]
+ public void SourceHelperBuildsBucketCommandsAndMapsExitCodes()
+ {
+ var manager = new Scoop();
+ var source = new SourceBuilder()
+ .WithManager(manager)
+ .WithName("extras")
+ .WithUrl("https://github.com/ScoopInstaller/Extras")
+ .Build();
+
+ Assert.Equal(
+ ["bucket", "add", "extras", "https://github.com/ScoopInstaller/Extras"],
+ manager.SourcesHelper.GetAddSourceParameters(source)
+ );
+ Assert.Equal(["bucket", "rm", "extras"], manager.SourcesHelper.GetRemoveSourceParameters(source));
+ Assert.Equal(
+ OperationVeredict.Success,
+ manager.SourcesHelper.GetAddOperationVeredict(source, 0, [])
+ );
+ Assert.Equal(
+ OperationVeredict.Failure,
+ manager.SourcesHelper.GetRemoveOperationVeredict(source, 1, [])
+ );
+ }
+
+ [Fact]
+ public void InstallParametersIncludeSourceScopeArchitectureAndHashFlags()
+ {
+ var manager = new Scoop();
+ var source = new SourceBuilder()
+ .WithManager(manager)
+ .WithName("extras")
+ .WithUrl("https://github.com/ScoopInstaller/Extras")
+ .Build();
+ manager.SourcesHelper.Factory.AddSource(source);
+ var package = new PackageBuilder().WithManager(manager).WithSource(source).WithId("git").Build();
+ var options = new InstallOptions
+ {
+ InstallationScope = PackageScope.Global,
+ Architecture = Architecture.x64,
+ SkipHashCheck = true,
+ CustomParameters_Install = ["--no-cache"],
+ };
+
+ var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Install);
+
+ OperationAssert.HasParameters(
+ parameters,
+ "install",
+ "extras/git",
+ "--global",
+ "--no-cache",
+ "--skip-hash-check",
+ "--arch",
+ "64bit"
+ );
+ Assert.True(package.OverridenOptions.RunAsAdministrator);
+ }
+
+ [Fact]
+ public void UninstallParametersOmitLocalSourcePrefixAndAppendPurge()
+ {
+ var manager = new Scoop();
+ var source = new SourceBuilder()
+ .WithManager(manager)
+ .WithName(@"C:\Buckets\custom")
+ .WithUrl("https://example.test/custom")
+ .Build();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithSource(source)
+ .WithId("custom-tool")
+ .Build();
+ var options = new InstallOptions
+ {
+ RemoveDataOnUninstall = true,
+ CustomParameters_Uninstall = ["--verbose"],
+ };
+
+ var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Uninstall);
+
+ OperationAssert.HasParameters(
+ parameters,
+ "uninstall",
+ "custom-tool",
+ "--verbose",
+ "--purge"
+ );
+ }
+
+ [Fact]
+ public void OperationResultPromotesGlobalRetryWhenScoopRequestsGlobalFlag()
+ {
+ var manager = new Scoop();
+ var package = new PackageBuilder().WithManager(manager).Build();
+
+ var veredict = manager.OperationHelper.GetResult(
+ package,
+ OperationType.Install,
+ ["Try again with the --global (or -g) flag instead"],
+ 1
+ );
+
+ OperationAssert.HasVeredict(veredict, OperationVeredict.AutoRetry);
+ Assert.Equal(PackageScope.Global, package.OverridenOptions.Scope);
+ Assert.True(package.OverridenOptions.RunAsAdministrator);
+ }
+
+ [Fact]
+ public void OperationResultPromotesElevationRetryBeforeReturningFailure()
+ {
+ var manager = new Scoop();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithOptions(new OverridenInstallationOptions(runAsAdministrator: false))
+ .Build();
+
+ var retry = manager.OperationHelper.GetResult(
+ package,
+ OperationType.Install,
+ ["package requires administrator rights"],
+ 1
+ );
+ var failure = manager.OperationHelper.GetResult(package, OperationType.Install, ["ERROR: failed"], 1);
+ var success = manager.OperationHelper.GetResult(package, OperationType.Install, ["done"], 0);
+
+ OperationAssert.HasVeredict(retry, OperationVeredict.AutoRetry);
+ Assert.True(package.OverridenOptions.RunAsAdministrator);
+ OperationAssert.HasVeredict(failure, OperationVeredict.Failure);
+ OperationAssert.HasVeredict(success, OperationVeredict.Success);
+ }
+
+ private static Scoop CreateManagerWithKnownSources(params string[] sourceNames)
+ {
+ var manager = new Scoop();
+ manager.SourcesHelper.Factory.AddSource(manager.DefaultSource);
+ foreach (string sourceName in sourceNames)
+ {
+ IManagerSource source =
+ manager.Properties.KnownSources.FirstOrDefault(source => source.Name == sourceName)
+ ?? new SourceBuilder()
+ .WithManager(manager)
+ .WithName(sourceName)
+ .WithUrl($"https://example.test/{sourceName}")
+ .Build();
+ manager.SourcesHelper.Factory.AddSource(source);
+ }
+
+ return manager;
+ }
+
+ private static string[] ReadFixtureLines(string relativePath)
+ {
+ return PackageEngineFixtureFiles.ReadAllText(relativePath).Replace("\r\n", "\n").Split('\n');
+ }
+}
+#endif
diff --git a/src/UniGetUI.PackageEngine.Tests/SourceOperationsTests.cs b/src/UniGetUI.PackageEngine.Tests/SourceOperationsTests.cs
new file mode 100644
index 0000000000..938d95b41a
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/SourceOperationsTests.cs
@@ -0,0 +1,180 @@
+using System.Diagnostics;
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.Operations;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageOperations;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+[Collection(nameof(OperationOrchestrationTestCollection))]
+public sealed class SourceOperationsTests
+{
+ [Fact]
+ public void RetryAsAdminSetsForceAsAdministrator()
+ {
+ var source = CreateSource();
+ using var operation = new InspectableAddSourceOperation(source);
+
+ operation.Retry(AbstractOperation.RetryMode.Retry_AsAdmin);
+
+ Assert.True(operation.ForceAsAdministrator);
+ Assert.Throws(
+ () => operation.Retry(AbstractOperation.RetryMode.Retry_Interactive)
+ );
+ }
+
+ [Fact]
+ public void CreateInstallAndUninstallPreOpsSkipNonWingetManagers()
+ {
+ var source = CreateSource();
+
+ Assert.Empty(SourceOperation.CreateInstallPreOps(source, forceLocalWinGet: false));
+ Assert.Empty(SourceOperation.CreateInstallPreOps(source, forceLocalWinGet: true));
+ Assert.Empty(SourceOperation.CreateUninstallPreOps(source, forceLocalWinGet: false));
+ Assert.Empty(SourceOperation.CreateUninstallPreOps(source, forceLocalWinGet: true));
+ }
+
+ [Fact]
+ public void AddSourcePrepareProcessStartInfoUsesManagerExecutableWithoutAdmin()
+ {
+ var manager = new PackageManagerBuilder()
+ .ConfigureManager(manager =>
+ {
+ manager.ExecutablePath = "C:\\tools\\sources.exe";
+ manager.ExecutableArguments = "--cli";
+ })
+ .ConfigureSources(helper => helper.AddParametersFactory = source => ["source", "add", source.Name])
+ .Build();
+ var source = new SourceBuilder().WithManager(manager).WithName("community").Build();
+ using var operation = new InspectableAddSourceOperation(source);
+ AbstractOperation.BadgeCollection? badges = null;
+ operation.BadgesChanged += (_, updatedBadges) => badges = updatedBadges;
+
+ var startInfo = operation.PrepareProcessStartInfoForTests();
+
+ Assert.Equal("C:\\tools\\sources.exe", startInfo.FileName);
+ Assert.Equal("--cli source add community", startInfo.Arguments.Trim());
+ Assert.NotNull(badges);
+ Assert.False(badges!.AsAdministrator);
+ }
+
+ [Fact]
+ public void RemoveSourcePrepareProcessStartInfoUsesElevatorWhenAdminRequired()
+ {
+ using var prohibitElevation = new BooleanSettingScope(Settings.K.ProhibitElevation, false);
+ var manager = new PackageManagerBuilder()
+ .ConfigureCapabilities(capabilities =>
+ {
+ var sourceCapabilities = capabilities.Sources;
+ sourceCapabilities.MustBeInstalledAsAdmin = true;
+ capabilities.Sources = sourceCapabilities;
+ return capabilities;
+ })
+ .ConfigureManager(manager =>
+ {
+ manager.ExecutablePath = "C:\\tools\\sources.exe";
+ manager.ExecutableArguments = "--cli";
+ })
+ .ConfigureSources(helper =>
+ helper.RemoveParametersFactory = source => ["source", "remove", source.Name]
+ )
+ .Build();
+ var source = new SourceBuilder().WithManager(manager).WithName("community").Build();
+ using var operation = new InspectableRemoveSourceOperation(source);
+ AbstractOperation.BadgeCollection? badges = null;
+ operation.BadgesChanged += (_, updatedBadges) => badges = updatedBadges;
+
+ var startInfo = operation.PrepareProcessStartInfoForTests();
+
+ Assert.Equal(CoreData.ElevatorPath, startInfo.FileName);
+ Assert.Equal("\"C:\\tools\\sources.exe\" --cli source remove community", startInfo.Arguments);
+ Assert.NotNull(badges);
+ Assert.True(badges!.AsAdministrator);
+ }
+
+ private static IManagerSource CreateSource()
+ {
+ return new SourceBuilder().WithManager(new PackageManagerBuilder().Build()).Build();
+ }
+
+ private sealed class InspectableAddSourceOperation : AddSourceOperation
+ {
+ public InspectableAddSourceOperation(IManagerSource source, bool forceLocalWinGet = false)
+ : base(source, forceLocalWinGet) { }
+
+ public ProcessStartInfo PrepareProcessStartInfoForTests()
+ {
+ InitializeProcessStartInfoDefaults();
+ PrepareProcessStartInfo();
+ return process.StartInfo;
+ }
+
+ private void InitializeProcessStartInfoDefaults()
+ {
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardInput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.StandardErrorEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.StandardInputEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.WorkingDirectory = Environment.GetFolderPath(
+ Environment.SpecialFolder.UserProfile
+ );
+ process.StartInfo.FileName = "lol";
+ process.StartInfo.Arguments = "lol";
+ }
+ }
+
+ private sealed class InspectableRemoveSourceOperation : RemoveSourceOperation
+ {
+ public InspectableRemoveSourceOperation(IManagerSource source, bool forceLocalWinGet = false)
+ : base(source, forceLocalWinGet) { }
+
+ public ProcessStartInfo PrepareProcessStartInfoForTests()
+ {
+ InitializeProcessStartInfoDefaults();
+ PrepareProcessStartInfo();
+ return process.StartInfo;
+ }
+
+ private void InitializeProcessStartInfoDefaults()
+ {
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardInput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.StandardErrorEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.StandardInputEncoding = System.Text.Encoding.UTF8;
+ process.StartInfo.WorkingDirectory = Environment.GetFolderPath(
+ Environment.SpecialFolder.UserProfile
+ );
+ process.StartInfo.FileName = "lol";
+ process.StartInfo.Arguments = "lol";
+ }
+ }
+
+ private sealed class BooleanSettingScope : IDisposable
+ {
+ private readonly Settings.K _key;
+ private readonly bool _originalValue;
+
+ public BooleanSettingScope(Settings.K key, bool value)
+ {
+ _key = key;
+ _originalValue = Settings.Get(key);
+ Settings.Set(key, value);
+ }
+
+ public void Dispose()
+ {
+ Settings.Set(_key, _originalValue);
+ }
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/TestAssembly.cs b/src/UniGetUI.PackageEngine.Tests/TestAssembly.cs
new file mode 100644
index 0000000000..217120083b
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/TestAssembly.cs
@@ -0,0 +1,3 @@
+using Xunit;
+
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
diff --git a/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj b/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj
new file mode 100644
index 0000000000..e2e837201a
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj
@@ -0,0 +1,55 @@
+
+
+
+ $(SharedTargetFrameworks)
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI.PackageEngine.Tests/UpgradablePackagesLoaderTests.cs b/src/UniGetUI.PackageEngine.Tests/UpgradablePackagesLoaderTests.cs
new file mode 100644
index 0000000000..f1aa6816e0
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/UpgradablePackagesLoaderTests.cs
@@ -0,0 +1,144 @@
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.Core.SettingsEngine.SecureSettings;
+using UniGetUI.Interface.Enums;
+using UniGetUI.PackageEngine.PackageClasses;
+using UniGetUI.PackageEngine.PackageLoader;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Structs;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Fakes;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+public sealed class UpgradablePackagesLoaderTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ Path.GetTempPath(),
+ nameof(UpgradablePackagesLoaderTests),
+ Guid.NewGuid().ToString("N")
+ );
+
+ public UpgradablePackagesLoaderTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ SecureSettings.TEST_SecureSettingsRootOverride = Path.Combine(_testRoot, "SecureSettings");
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Directory.CreateDirectory(CoreData.UniGetUIInstallationOptionsDirectory);
+ Settings.ResetSettings();
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ SecureSettings.TEST_SecureSettingsRootOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public async Task EvaluatePackageAsync_SkipsMinorUpdatesWhenConfigured()
+ {
+ var manager = new PackageManagerBuilder().Build();
+ _ = new InstalledPackagesLoader([manager]);
+ _ = new DiscoverablePackagesLoader([manager]);
+ var loader = new TestUpgradablePackagesLoader([manager]);
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tool")
+ .WithVersion("1.0.0")
+ .WithNewVersion("1.0.1")
+ .Build();
+
+ InstallOptionsFactory.SaveForPackage(
+ new InstallOptions
+ {
+ OverridesNextLevelOpts = true,
+ SkipMinorUpdates = true,
+ },
+ package
+ );
+
+ Assert.False(await loader.EvaluatePackageAsync(package));
+ }
+
+ [Fact]
+ public async Task EvaluatePackageAsync_SkipsIgnoredAndSupersededPackages()
+ {
+ var manager = new PackageManagerBuilder().Build();
+ var installedLoader = new InstalledPackagesLoader([manager]);
+ _ = new DiscoverablePackagesLoader([manager]);
+ var loader = new TestUpgradablePackagesLoader([manager]);
+ var ignoredPackage = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Ignored")
+ .WithVersion("1.0.0")
+ .WithNewVersion("2.0.0")
+ .Build();
+ var supersededPackage = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Superseded")
+ .WithVersion("1.0.0")
+ .WithNewVersion("2.0.0")
+ .Build();
+
+ await ignoredPackage.AddToIgnoredUpdatesAsync("2.0.0");
+ await installedLoader.AddForeign(
+ new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Superseded")
+ .WithVersion("3.0.0")
+ .Build()
+ );
+
+ Assert.False(await loader.EvaluatePackageAsync(ignoredPackage));
+ Assert.Contains("Contoso.Ignored", loader.IgnoredPackages.Keys);
+ Assert.False(await loader.EvaluatePackageAsync(supersededPackage));
+ }
+
+ [Fact]
+ public async Task ApplyWhenAddingPackageAsync_UpdatesDiscoverableAndInstalledTags()
+ {
+ var manager = new PackageManagerBuilder().Build();
+ var installedLoader = new InstalledPackagesLoader([manager]);
+ var discoverableLoader = new DiscoverablePackagesLoader([manager]);
+ var loader = new TestUpgradablePackagesLoader([manager]);
+ var availablePackage = new PackageBuilder().WithManager(manager).WithId("Contoso.Tagged").Build();
+ var installedPackage = new PackageBuilder().WithManager(manager).WithId("Contoso.Tagged").Build();
+ var upgradablePackage = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("Contoso.Tagged")
+ .WithVersion("1.0.0")
+ .WithNewVersion("2.0.0")
+ .Build();
+
+ await discoverableLoader.AddForeign(availablePackage);
+ await installedLoader.AddForeign(installedPackage);
+
+ await loader.ApplyWhenAddingPackageAsync(upgradablePackage);
+
+ Assert.Equal(PackageTag.IsUpgradable, availablePackage.Tag);
+ Assert.Equal(PackageTag.IsUpgradable, installedPackage.Tag);
+ }
+
+ [Theory]
+ [InlineData("120", 120000)]
+ [InlineData("not-a-number", 3600000)]
+ public void StartTimer_UsesConfiguredIntervalOrDefault(string configuredValue, double expectedInterval)
+ {
+ var manager = new PackageManagerBuilder().Build();
+ _ = new InstalledPackagesLoader([manager]);
+ _ = new DiscoverablePackagesLoader([manager]);
+ var loader = new TestUpgradablePackagesLoader([manager]);
+ Settings.Set(Settings.K.DisableAutoCheckforUpdates, false);
+ Settings.SetValue(Settings.K.UpdatesCheckInterval, configuredValue);
+
+ loader.StartTimer();
+
+ Assert.Equal(expectedInterval, loader.GetTimerIntervalMilliseconds());
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs
new file mode 100644
index 0000000000..6ed59ba048
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs
@@ -0,0 +1,192 @@
+#if WINDOWS
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.PackageEngine.Classes.Manager;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.Managers.WingetManager;
+using UniGetUI.PackageEngine.PackageClasses;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+[CollectionDefinition("WinGet manager tests", DisableParallelization = true)]
+public sealed class WinGetManagerTestCollection
+{
+ public const string Name = "WinGet manager tests";
+}
+
+[Collection(WinGetManagerTestCollection.Name)]
+public sealed class WinGetManagerTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ AppContext.BaseDirectory,
+ "WinGetManagerTests",
+ Guid.NewGuid().ToString("N")
+ );
+
+ public WinGetManagerTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ SetNoPackagesHaveBeenLoaded(false);
+ Settings.Set(Settings.K.EnableProxy, false);
+ Settings.Set(Settings.K.EnableProxyAuth, false);
+ Settings.SetValue(Settings.K.ProxyURL, "");
+ }
+
+ public void Dispose()
+ {
+ SetNoPackagesHaveBeenLoaded(false);
+ WinGetHelper.Instance = null!;
+ CoreData.TEST_DataDirectoryOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void GetProxyArgumentReturnsEmptyStringWhenProxyIsDisabled()
+ {
+ Settings.Set(Settings.K.EnableProxy, false);
+ Settings.SetValue(Settings.K.ProxyURL, "http://proxy.example.test:3128/");
+
+ Assert.Equal("", WinGet.GetProxyArgument());
+ }
+
+ [Fact]
+ public void GetProxyArgumentReturnsTrimmedProxyArgumentWhenProxyIsEnabled()
+ {
+ Settings.Set(Settings.K.EnableProxy, true);
+ Settings.Set(Settings.K.EnableProxyAuth, false);
+ Settings.SetValue(Settings.K.ProxyURL, "http://proxy.example.test:3128/");
+
+ Assert.Equal("--proxy http://proxy.example.test:3128", WinGet.GetProxyArgument());
+ }
+
+ [Fact]
+ public void GetProxyArgumentReturnsEmptyStringWhenProxyAuthIsEnabled()
+ {
+ Settings.Set(Settings.K.EnableProxy, true);
+ Settings.Set(Settings.K.EnableProxyAuth, true);
+ Settings.SetValue(Settings.K.ProxyURL, "http://proxy.example.test:3128/");
+
+ Assert.Equal("", WinGet.GetProxyArgument());
+ }
+
+ [Theory]
+ [MemberData(nameof(LocalSourceCases))]
+ public void GetLocalSourceClassifiesKnownSourceFamilies(
+ string id,
+ LocalSourceKind expectedSourceKind
+ )
+ {
+ var manager = new WinGet();
+
+ var source = manager.GetLocalSource(id);
+
+ Assert.Same(GetExpectedSource(manager, expectedSourceKind), source);
+ }
+
+ public static TheoryData LocalSourceCases =>
+ new()
+ {
+ { "MSIX\\Microsoft.WindowsStore_8wekyb3d8bbwe", LocalSourceKind.MicrosoftStore },
+ { "Programs\\{12345678-1234-1234-1234-123456789ABC}", LocalSourceKind.LocalPc },
+ { "Apps\\com.example.android.app", LocalSourceKind.Android },
+ { "Games\\Steam", LocalSourceKind.Steam },
+ { "Games\\Steam App 12345", LocalSourceKind.Steam },
+ { "Games\\Uplay", LocalSourceKind.Ubisoft },
+ { "Games\\Uplay Install 12345", LocalSourceKind.Ubisoft },
+ { "Games\\123456789_is1", LocalSourceKind.Gog },
+ { "Programs\\Contoso.App", LocalSourceKind.LocalPc },
+ };
+
+ [Fact]
+ public void GetInstalledPackagesUpdatesNoPackagesFlagForFailureAndRecovery()
+ {
+ var manager = new TestableWinGet();
+ var expectedPackage = new PackageBuilder()
+ .WithManager(manager)
+ .WithName("Contoso Tool")
+ .WithId("Contoso.Tool")
+ .WithVersion("1.2.3")
+ .Build();
+ var helper = new TestWinGetManagerHelper
+ {
+ GetInstalledPackagesHandler = () => throw new InvalidOperationException("boom"),
+ };
+ WinGetHelper.Instance = helper;
+
+ Assert.Throws(manager.InvokeGetInstalledPackages);
+ Assert.True(WinGet.NO_PACKAGES_HAVE_BEEN_LOADED);
+
+ helper.GetInstalledPackagesHandler = () => [expectedPackage];
+
+ var packages = manager.InvokeGetInstalledPackages();
+
+ Assert.False(WinGet.NO_PACKAGES_HAVE_BEEN_LOADED);
+ PackageAssert.Matches(Assert.Single(packages), "Contoso Tool", "Contoso.Tool", "1.2.3");
+ }
+
+ private sealed class TestableWinGet : WinGet
+ {
+ public IReadOnlyList InvokeGetInstalledPackages() => base.GetInstalledPackages_UnSafe();
+ }
+
+ private static IManagerSource GetExpectedSource(WinGet manager, LocalSourceKind expectedSourceKind) =>
+ expectedSourceKind switch
+ {
+ LocalSourceKind.MicrosoftStore => manager.MicrosoftStoreSource,
+ LocalSourceKind.LocalPc => manager.LocalPcSource,
+ LocalSourceKind.Android => manager.AndroidSubsystemSource,
+ LocalSourceKind.Steam => manager.SteamSource,
+ LocalSourceKind.Ubisoft => manager.UbisoftConnectSource,
+ LocalSourceKind.Gog => manager.GOGSource,
+ _ => throw new ArgumentOutOfRangeException(nameof(expectedSourceKind)),
+ };
+
+ private static void SetNoPackagesHaveBeenLoaded(bool value)
+ {
+ typeof(WinGet)
+ .GetProperty(nameof(WinGet.NO_PACKAGES_HAVE_BEEN_LOADED))!
+ .GetSetMethod(nonPublic: true)!
+ .Invoke(null, [value]);
+ }
+
+ public enum LocalSourceKind
+ {
+ MicrosoftStore,
+ LocalPc,
+ Android,
+ Steam,
+ Ubisoft,
+ Gog,
+ }
+
+ private sealed class TestWinGetManagerHelper : IWinGetManagerHelper
+ {
+ public Func> GetAvailableUpdatesHandler { get; set; } = static () => [];
+ public Func> GetInstalledPackagesHandler { get; set; } = static () => [];
+ public Func> FindPackagesHandler { get; set; } = static _ => [];
+ public Func> GetSourcesHandler { get; set; } = static () => [];
+ public Func> GetInstallableVersionsHandler { get; set; } =
+ static _ => [];
+ public Action GetPackageDetailsHandler { get; set; } = static _ => { };
+
+ public IReadOnlyList GetAvailableUpdates_UnSafe() => GetAvailableUpdatesHandler();
+
+ public IReadOnlyList GetInstalledPackages_UnSafe() => GetInstalledPackagesHandler();
+
+ public IReadOnlyList FindPackages_UnSafe(string query) => FindPackagesHandler(query);
+
+ public IReadOnlyList GetSources_UnSafe() => GetSourcesHandler();
+
+ public IReadOnlyList GetInstallableVersions_Unsafe(IPackage package) =>
+ GetInstallableVersionsHandler(package);
+
+ public void GetPackageDetails_UnSafe(IPackageDetails details) => GetPackageDetailsHandler(details);
+ }
+}
+#endif
diff --git a/src/UniGetUI.Tests/AutoUpdaterTests.cs b/src/UniGetUI.Tests/AutoUpdaterTests.cs
new file mode 100644
index 0000000000..6a4d3ca847
--- /dev/null
+++ b/src/UniGetUI.Tests/AutoUpdaterTests.cs
@@ -0,0 +1,101 @@
+using System.Runtime.InteropServices;
+using Microsoft.Win32;
+
+namespace UniGetUI.Tests;
+
+public sealed class AutoUpdaterTests
+{
+ [Theory]
+ [InlineData("https://devolutions.net/productinfo.json", false, true)]
+ [InlineData("https://updates.devolutions.net/productinfo.json", false, true)]
+ [InlineData("https://notdevolutions.net/productinfo.json", false, false)]
+ [InlineData("https://github.com/Devolutions/UniGetUI/releases", false, true)]
+ [InlineData("http://devolutions.net/productinfo.json", false, false)]
+ [InlineData("http://contoso.invalid/file.exe", true, true)]
+ public void IsSourceUrlAllowed_RestrictsUnsafeOrUnexpectedHosts(
+ string url,
+ bool allowUnsafeUrls,
+ bool expected
+ )
+ {
+ Assert.Equal(expected, AutoUpdater.IsSourceUrlAllowed(url, allowUnsafeUrls));
+ }
+
+ [Fact]
+ public void SelectInstallerFile_PrefersExecutableForCurrentArchitecture()
+ {
+ string targetArch = RuntimeInformation.ProcessArchitecture switch
+ {
+ Architecture.Arm64 => "arm64",
+ Architecture.X64 => "x64",
+ _ => "x64",
+ };
+ var preferred = new AutoUpdater.ProductInfoFile
+ {
+ Arch = targetArch,
+ Type = "exe",
+ Url = "https://example.test/app.exe",
+ Hash = "hash-exe",
+ };
+
+ var selected = AutoUpdater.SelectInstallerFile(
+ [
+ new AutoUpdater.ProductInfoFile
+ {
+ Arch = "Any",
+ Type = "exe",
+ Url = "https://example.test/any.exe",
+ Hash = "hash-any",
+ },
+ new AutoUpdater.ProductInfoFile
+ {
+ Arch = targetArch,
+ Type = "msi",
+ Url = "https://example.test/app.msi",
+ Hash = "hash-msi",
+ },
+ preferred,
+ ]
+ );
+
+ Assert.Same(preferred, selected);
+ }
+
+ [Fact]
+ public void ParseVersionOrFallback_ParsesTrimmedVersionsAndFallsBackForInvalidInput()
+ {
+ Version fallback = new(9, 9, 9, 9);
+
+ Assert.Equal(new Version(1, 2, 3), AutoUpdater.ParseVersionOrFallback("v1.2.3", fallback));
+ Assert.Equal(fallback, AutoUpdater.ParseVersionOrFallback("not-a-version", fallback));
+ }
+
+ [Fact]
+ public void NormalizeThumbprint_RemovesNonHexCharactersAndLowercases()
+ {
+ Assert.Equal("abcdef1234", AutoUpdater.NormalizeThumbprint("AB:CD ef-12_34"));
+ }
+
+#if DEBUG
+ [Fact]
+ public void RegistryHelpers_ParseTrimmedStringsAndTruthyValues()
+ {
+ string keyPath = $@"Software\Devolutions\UniGetUI.Tests\{Guid.NewGuid():N}";
+ using RegistryKey key = Registry.CurrentUser.CreateSubKey(keyPath)!;
+ key.SetValue("ProductInfoUrl", " https://devolutions.net/custom.json ");
+ key.SetValue("AllowUnsafe", "yes");
+
+ try
+ {
+ Assert.Equal("https://devolutions.net/custom.json", AutoUpdater.GetRegistryString(key, "ProductInfoUrl"));
+ Assert.True(AutoUpdater.GetRegistryBool(key, "AllowUnsafe"));
+ Assert.Null(AutoUpdater.GetRegistryString(key, "Missing"));
+ Assert.False(AutoUpdater.GetRegistryBool(key, "Missing"));
+ }
+ finally
+ {
+ Registry.CurrentUser.DeleteSubKeyTree(keyPath, throwOnMissingSubKey: false);
+ }
+ }
+#endif
+}
diff --git a/src/UniGetUI.Tests/CLIHandlerTests.cs b/src/UniGetUI.Tests/CLIHandlerTests.cs
new file mode 100644
index 0000000000..4f011be11e
--- /dev/null
+++ b/src/UniGetUI.Tests/CLIHandlerTests.cs
@@ -0,0 +1,118 @@
+using System.Text.Json;
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.Core.SettingsEngine.SecureSettings;
+
+namespace UniGetUI.Tests;
+
+public sealed class CLIHandlerTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ Path.GetTempPath(),
+ nameof(CLIHandlerTests),
+ Guid.NewGuid().ToString("N")
+ );
+ private readonly string _secureSettingsRoot;
+
+ public CLIHandlerTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ _secureSettingsRoot = Path.Combine(_testRoot, "SecureSettings");
+ SecureSettings.TEST_SecureSettingsRootOverride = _secureSettingsRoot;
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Settings.ResetSettings();
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ SecureSettings.TEST_SecureSettingsRootOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ImportSettings_ReturnsNoSuchFileWhenInputIsMissing()
+ {
+ int result = CLIHandler.ImportSettings(["unigetui", CLIHandler.IMPORT_SETTINGS, Path.Combine(_testRoot, "missing.json")]);
+
+ Assert.Equal(-1073741809, result);
+ }
+
+ [Fact]
+ public void ExportAndImportSettings_RoundTripConfiguration()
+ {
+ string exportPath = Path.Combine(_testRoot, "settings.json");
+ Settings.Set(Settings.K.FreshBoolSetting, true);
+ Settings.SetValue(Settings.K.FreshValue, "before-export");
+
+ Assert.Equal(0, CLIHandler.ExportSettings(["unigetui", CLIHandler.EXPORT_SETTINGS, exportPath]));
+
+ Settings.Set(Settings.K.FreshBoolSetting, false);
+ Settings.SetValue(Settings.K.FreshValue, "after-export");
+
+ Assert.Equal(0, CLIHandler.ImportSettings(["unigetui", CLIHandler.IMPORT_SETTINGS, exportPath]));
+ Assert.True(Settings.Get(Settings.K.FreshBoolSetting));
+ Assert.Equal("before-export", Settings.GetValue(Settings.K.FreshValue));
+
+ var exported = JsonSerializer.Deserialize>(File.ReadAllText(exportPath));
+ Assert.NotNull(exported);
+ Assert.Equal("before-export", exported[Settings.ResolveKey(Settings.K.FreshValue)]);
+ }
+
+ [Fact]
+ public void EnableDisableAndSetValue_MutateSettings()
+ {
+ Assert.Equal(0, CLIHandler.EnableSetting(["unigetui", CLIHandler.ENABLE_SETTING, nameof(Settings.K.Test1)]));
+ Assert.True(Settings.Get(Settings.K.Test1));
+
+ Assert.Equal(0, CLIHandler.SetSettingsValue(["unigetui", CLIHandler.SET_SETTING_VAL, nameof(Settings.K.FreshValue), "cli-value"]));
+ Assert.Equal("cli-value", Settings.GetValue(Settings.K.FreshValue));
+
+ Assert.Equal(0, CLIHandler.DisableSetting(["unigetui", CLIHandler.DISABLE_SETTING, nameof(Settings.K.Test1)]));
+ Assert.False(Settings.Get(Settings.K.Test1));
+ }
+
+ [Fact]
+ public void EnableAndDisableSecureSettingForUser_MutateSecureSettings()
+ {
+ string user = "cli-user";
+ string setting = "AllowCLIArguments";
+
+ Assert.Equal(
+ 0,
+ CLIHandler.EnableSecureSettingForUser(
+ ["unigetui", CLIHandler.ENABLE_SECURE_SETTING_FOR_USER, user, setting]
+ )
+ );
+ Assert.True(
+ File.Exists(
+ Path.Combine(
+ _secureSettingsRoot,
+ user,
+ setting
+ )
+ )
+ );
+
+ Assert.Equal(
+ 0,
+ CLIHandler.DisableSecureSettingForUser(
+ ["unigetui", CLIHandler.DISABLE_SECURE_SETTING_FOR_USER, user, setting]
+ )
+ );
+ Assert.False(
+ File.Exists(
+ Path.Combine(
+ _secureSettingsRoot,
+ user,
+ setting
+ )
+ )
+ );
+ }
+}
diff --git a/src/UniGetUI.Tests/TestAssembly.cs b/src/UniGetUI.Tests/TestAssembly.cs
new file mode 100644
index 0000000000..217120083b
--- /dev/null
+++ b/src/UniGetUI.Tests/TestAssembly.cs
@@ -0,0 +1,3 @@
+using Xunit;
+
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
diff --git a/src/UniGetUI.Tests/UniGetUI.Tests.csproj b/src/UniGetUI.Tests/UniGetUI.Tests.csproj
new file mode 100644
index 0000000000..1a205ca3b8
--- /dev/null
+++ b/src/UniGetUI.Tests/UniGetUI.Tests.csproj
@@ -0,0 +1,49 @@
+
+
+ $(WindowsTargetFramework)
+
+ false
+ true
+ x64
+ win-x64
+ false
+ false
+ false
+ false
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI.sln b/src/UniGetUI.sln
index 88508377d4..d64b90697c 100644
--- a/src/UniGetUI.sln
+++ b/src/UniGetUI.sln
@@ -109,350 +109,746 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniGetUI.PackageEngine.Seri
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniGetUI.Core.SecureSettings", "UniGetUI.Core.SecureSettings\UniGetUI.Core.SecureSettings.csproj", "{B0E59327-933E-4DB0-BD2D-FB16EB9B4194}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniGetUI.PackageEngine.Tests", "UniGetUI.PackageEngine.Tests\UniGetUI.PackageEngine.Tests.csproj", "{181E4491-B96D-4E33-8102-AC91420D6123}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniGetUI.Interface.Telemetry.Tests", "UniGetUI.Interface.Telemetry.Tests\UniGetUI.Interface.Telemetry.Tests.csproj", "{991DBBC6-3E13-4A95-BCDC-F99512F87DC3}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UniGetUI.Core.Logger", "UniGetUI.Core.Logger", "{8531793E-F891-2283-BD05-9B92EE2A6C73}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniGetUI.Tests", "UniGetUI.Tests\UniGetUI.Tests.csproj", "{8CDDF549-38D0-45DB-9494-3B9B3376E7C0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|arm64 = Debug|arm64
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|arm64 = Release|arm64
+ Release|Any CPU = Release|Any CPU
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|x64.ActiveCfg = Debug|x64
- {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|arm64.ActiveCfg = Debug|arm64
{80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|x64.Build.0 = Debug|x64
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|arm64.ActiveCfg = Debug|arm64
{80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|arm64.Build.0 = Debug|arm64
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Debug|x86.Build.0 = Debug|Any CPU
{80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|x64.ActiveCfg = Release|x64
- {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|arm64.ActiveCfg = Release|arm64
{80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|x64.Build.0 = Release|x64
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|arm64.ActiveCfg = Release|arm64
{80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|arm64.Build.0 = Release|arm64
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|x86.ActiveCfg = Release|Any CPU
+ {80305A17-2534-48DC-8F75-41F70FCCEAAF}.Release|x86.Build.0 = Release|Any CPU
{52AC982E-7382-4746-BB66-4003698FCC02}.Debug|x64.ActiveCfg = Debug|x64
- {52AC982E-7382-4746-BB66-4003698FCC02}.Debug|arm64.ActiveCfg = Debug|arm64
{52AC982E-7382-4746-BB66-4003698FCC02}.Debug|x64.Build.0 = Debug|x64
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Debug|arm64.ActiveCfg = Debug|arm64
{52AC982E-7382-4746-BB66-4003698FCC02}.Debug|arm64.Build.0 = Debug|arm64
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Debug|x86.Build.0 = Debug|Any CPU
{52AC982E-7382-4746-BB66-4003698FCC02}.Release|x64.ActiveCfg = Release|x64
- {52AC982E-7382-4746-BB66-4003698FCC02}.Release|arm64.ActiveCfg = Release|arm64
{52AC982E-7382-4746-BB66-4003698FCC02}.Release|x64.Build.0 = Release|x64
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Release|arm64.ActiveCfg = Release|arm64
{52AC982E-7382-4746-BB66-4003698FCC02}.Release|arm64.Build.0 = Release|arm64
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Release|Any CPU.Build.0 = Release|Any CPU
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Release|x86.ActiveCfg = Release|Any CPU
+ {52AC982E-7382-4746-BB66-4003698FCC02}.Release|x86.Build.0 = Release|Any CPU
{5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|x64.ActiveCfg = Debug|x64
- {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|arm64.ActiveCfg = Debug|arm64
{5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|x64.Build.0 = Debug|x64
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|arm64.ActiveCfg = Debug|arm64
{5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|arm64.Build.0 = Debug|arm64
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Debug|x86.Build.0 = Debug|Any CPU
{5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|x64.ActiveCfg = Release|x64
- {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|arm64.ActiveCfg = Release|arm64
{5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|x64.Build.0 = Release|x64
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|arm64.ActiveCfg = Release|arm64
{5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|arm64.Build.0 = Release|arm64
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|x86.ActiveCfg = Release|Any CPU
+ {5F5EF76B-D755-4C12-ADAE-11F08CE3D936}.Release|x86.Build.0 = Release|Any CPU
{B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|x64.ActiveCfg = Debug|x64
- {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|arm64.ActiveCfg = Debug|arm64
{B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|x64.Build.0 = Debug|x64
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|arm64.ActiveCfg = Debug|arm64
{B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|arm64.Build.0 = Debug|arm64
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Debug|x86.Build.0 = Debug|Any CPU
{B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|x64.ActiveCfg = Release|x64
- {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|arm64.ActiveCfg = Release|arm64
{B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|x64.Build.0 = Release|x64
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|arm64.ActiveCfg = Release|arm64
{B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|arm64.Build.0 = Release|arm64
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|x86.ActiveCfg = Release|Any CPU
+ {B70A6F17-08C8-4194-BBE8-668CA920CFF3}.Release|x86.Build.0 = Release|Any CPU
{72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|x64.ActiveCfg = Debug|x64
- {72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|arm64.ActiveCfg = Debug|arm64
{72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|x64.Build.0 = Debug|x64
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|arm64.ActiveCfg = Debug|arm64
{72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|arm64.Build.0 = Debug|arm64
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Debug|x86.Build.0 = Debug|Any CPU
{72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|x64.ActiveCfg = Release|x64
- {72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|arm64.ActiveCfg = Release|arm64
{72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|x64.Build.0 = Release|x64
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|arm64.ActiveCfg = Release|arm64
{72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|arm64.Build.0 = Release|arm64
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|x86.ActiveCfg = Release|Any CPU
+ {72180B0C-3D20-4AAD-B015-A9337B91406E}.Release|x86.Build.0 = Release|Any CPU
{1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|x64.ActiveCfg = Debug|x64
- {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|arm64.ActiveCfg = Debug|arm64
{1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|x64.Build.0 = Debug|x64
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|arm64.ActiveCfg = Debug|arm64
{1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|arm64.Build.0 = Debug|arm64
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Debug|x86.Build.0 = Debug|Any CPU
{1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|x64.ActiveCfg = Release|x64
- {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|arm64.ActiveCfg = Release|arm64
{1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|x64.Build.0 = Release|x64
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|arm64.ActiveCfg = Release|arm64
{1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|arm64.Build.0 = Release|arm64
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|x86.ActiveCfg = Release|Any CPU
+ {1977360F-2E42-45E6-9369-AB1EE59CC5C5}.Release|x86.Build.0 = Release|Any CPU
{25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|x64.ActiveCfg = Debug|x64
- {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|arm64.ActiveCfg = Debug|arm64
{25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|x64.Build.0 = Debug|x64
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|arm64.ActiveCfg = Debug|arm64
{25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|arm64.Build.0 = Debug|arm64
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Debug|x86.Build.0 = Debug|Any CPU
{25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|x64.ActiveCfg = Release|x64
- {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|arm64.ActiveCfg = Release|arm64
{25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|x64.Build.0 = Release|x64
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|arm64.ActiveCfg = Release|arm64
{25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|arm64.Build.0 = Release|arm64
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|Any CPU.Build.0 = Release|Any CPU
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|x86.ActiveCfg = Release|Any CPU
+ {25C6CE64-2D61-4832-B6D2-45AFC52E2447}.Release|x86.Build.0 = Release|Any CPU
{8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|x64.ActiveCfg = Debug|x64
- {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|arm64.ActiveCfg = Debug|arm64
{8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|x64.Build.0 = Debug|x64
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|arm64.ActiveCfg = Debug|arm64
{8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|arm64.Build.0 = Debug|arm64
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Debug|x86.Build.0 = Debug|Any CPU
{8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|x64.ActiveCfg = Release|x64
- {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|arm64.ActiveCfg = Release|arm64
{8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|x64.Build.0 = Release|x64
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|arm64.ActiveCfg = Release|arm64
{8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|arm64.Build.0 = Release|arm64
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|x86.ActiveCfg = Release|Any CPU
+ {8156B6D8-BD7E-4201-BD8B-8C9B00177F88}.Release|x86.Build.0 = Release|Any CPU
{990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|x64.ActiveCfg = Debug|x64
- {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|arm64.ActiveCfg = Debug|arm64
{990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|x64.Build.0 = Debug|x64
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|arm64.ActiveCfg = Debug|arm64
{990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|arm64.Build.0 = Debug|arm64
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Debug|x86.Build.0 = Debug|Any CPU
{990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|x64.ActiveCfg = Release|x64
- {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|arm64.ActiveCfg = Release|arm64
{990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|x64.Build.0 = Release|x64
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|arm64.ActiveCfg = Release|arm64
{990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|arm64.Build.0 = Release|arm64
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|x86.ActiveCfg = Release|Any CPU
+ {990F5AFF-ABF6-4019-865D-604D2B23DE2C}.Release|x86.Build.0 = Release|Any CPU
{380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|x64.ActiveCfg = Debug|x64
- {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|arm64.ActiveCfg = Debug|arm64
{380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|x64.Build.0 = Debug|x64
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|arm64.ActiveCfg = Debug|arm64
{380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|arm64.Build.0 = Debug|arm64
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Debug|x86.Build.0 = Debug|Any CPU
{380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|x64.ActiveCfg = Release|x64
- {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|arm64.ActiveCfg = Release|arm64
{380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|x64.Build.0 = Release|x64
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|arm64.ActiveCfg = Release|arm64
{380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|arm64.Build.0 = Release|arm64
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|x86.ActiveCfg = Release|Any CPU
+ {380E9F5A-23DE-4F5A-9644-EFA51AD1D8E8}.Release|x86.Build.0 = Release|Any CPU
{5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|x64.ActiveCfg = Debug|x64
- {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|arm64.ActiveCfg = Debug|arm64
{5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|x64.Build.0 = Debug|x64
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|arm64.ActiveCfg = Debug|arm64
{5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|arm64.Build.0 = Debug|arm64
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Debug|x86.Build.0 = Debug|Any CPU
{5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|x64.ActiveCfg = Release|x64
- {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|arm64.ActiveCfg = Release|arm64
{5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|x64.Build.0 = Release|x64
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|arm64.ActiveCfg = Release|arm64
{5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|arm64.Build.0 = Release|arm64
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|x86.ActiveCfg = Release|Any CPU
+ {5A48C2FD-16E4-4B44-BC2C-D793C50E66F2}.Release|x86.Build.0 = Release|Any CPU
{9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|x64.ActiveCfg = Debug|x64
- {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|arm64.ActiveCfg = Debug|arm64
{9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|x64.Build.0 = Debug|x64
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|arm64.ActiveCfg = Debug|arm64
{9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|arm64.Build.0 = Debug|arm64
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Debug|x86.Build.0 = Debug|Any CPU
{9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|x64.ActiveCfg = Release|x64
- {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|arm64.ActiveCfg = Release|arm64
{9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|x64.Build.0 = Release|x64
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|arm64.ActiveCfg = Release|arm64
{9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|arm64.Build.0 = Release|arm64
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|x86.ActiveCfg = Release|Any CPU
+ {9AD1DEC9-1561-4753-AB4B-E81FBDBA5C9E}.Release|x86.Build.0 = Release|Any CPU
{E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|x64.ActiveCfg = Debug|x64
- {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|arm64.ActiveCfg = Debug|arm64
{E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|x64.Build.0 = Debug|x64
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|arm64.ActiveCfg = Debug|arm64
{E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|arm64.Build.0 = Debug|arm64
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Debug|x86.Build.0 = Debug|Any CPU
{E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|x64.ActiveCfg = Release|x64
- {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|arm64.ActiveCfg = Release|arm64
{E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|x64.Build.0 = Release|x64
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|arm64.ActiveCfg = Release|arm64
{E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|arm64.Build.0 = Release|arm64
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|x86.ActiveCfg = Release|Any CPU
+ {E40BFCBB-7A02-4E2C-AFDB-A717359EF4FC}.Release|x86.Build.0 = Release|Any CPU
{562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|x64.ActiveCfg = Debug|x64
- {562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|arm64.ActiveCfg = Debug|arm64
{562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|x64.Build.0 = Debug|x64
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|arm64.ActiveCfg = Debug|arm64
{562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|arm64.Build.0 = Debug|arm64
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Debug|x86.Build.0 = Debug|Any CPU
{562B4814-2A78-4692-90BE-A727AABCEC85}.Release|x64.ActiveCfg = Release|x64
- {562B4814-2A78-4692-90BE-A727AABCEC85}.Release|arm64.ActiveCfg = Release|arm64
{562B4814-2A78-4692-90BE-A727AABCEC85}.Release|x64.Build.0 = Release|x64
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Release|arm64.ActiveCfg = Release|arm64
{562B4814-2A78-4692-90BE-A727AABCEC85}.Release|arm64.Build.0 = Release|arm64
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Release|Any CPU.Build.0 = Release|Any CPU
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Release|x86.ActiveCfg = Release|Any CPU
+ {562B4814-2A78-4692-90BE-A727AABCEC85}.Release|x86.Build.0 = Release|Any CPU
{1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|x64.ActiveCfg = Debug|x64
- {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|arm64.ActiveCfg = Debug|arm64
{1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|x64.Build.0 = Debug|x64
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|arm64.ActiveCfg = Debug|arm64
{1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|arm64.Build.0 = Debug|arm64
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Debug|x86.Build.0 = Debug|Any CPU
{1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|x64.ActiveCfg = Release|x64
- {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|arm64.ActiveCfg = Release|arm64
{1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|x64.Build.0 = Release|x64
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|arm64.ActiveCfg = Release|arm64
{1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|arm64.Build.0 = Release|arm64
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|x86.ActiveCfg = Release|Any CPU
+ {1A51EA31-6D78-4E98-B767-41A02C6E34D8}.Release|x86.Build.0 = Release|Any CPU
{230BF08C-C039-473B-933F-3BF647440E0E}.Debug|x64.ActiveCfg = Debug|x64
- {230BF08C-C039-473B-933F-3BF647440E0E}.Debug|arm64.ActiveCfg = Debug|arm64
{230BF08C-C039-473B-933F-3BF647440E0E}.Debug|x64.Build.0 = Debug|x64
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Debug|arm64.ActiveCfg = Debug|arm64
{230BF08C-C039-473B-933F-3BF647440E0E}.Debug|arm64.Build.0 = Debug|arm64
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Debug|x86.Build.0 = Debug|Any CPU
{230BF08C-C039-473B-933F-3BF647440E0E}.Release|x64.ActiveCfg = Release|x64
- {230BF08C-C039-473B-933F-3BF647440E0E}.Release|arm64.ActiveCfg = Release|arm64
{230BF08C-C039-473B-933F-3BF647440E0E}.Release|x64.Build.0 = Release|x64
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Release|arm64.ActiveCfg = Release|arm64
{230BF08C-C039-473B-933F-3BF647440E0E}.Release|arm64.Build.0 = Release|arm64
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Release|x86.ActiveCfg = Release|Any CPU
+ {230BF08C-C039-473B-933F-3BF647440E0E}.Release|x86.Build.0 = Release|Any CPU
{C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|x64.ActiveCfg = Debug|x64
- {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|arm64.ActiveCfg = Debug|arm64
{C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|x64.Build.0 = Debug|x64
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|arm64.ActiveCfg = Debug|arm64
{C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|arm64.Build.0 = Debug|arm64
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Debug|x86.Build.0 = Debug|Any CPU
{C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|x64.ActiveCfg = Release|x64
- {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|arm64.ActiveCfg = Release|arm64
{C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|x64.Build.0 = Release|x64
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|arm64.ActiveCfg = Release|arm64
{C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|arm64.Build.0 = Release|arm64
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|x86.ActiveCfg = Release|Any CPU
+ {C55F4BA7-BBDD-42A4-88C1-FD3C411EB234}.Release|x86.Build.0 = Release|Any CPU
{2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|x64.ActiveCfg = Debug|x64
- {2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|arm64.ActiveCfg = Debug|arm64
{2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|x64.Build.0 = Debug|x64
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|arm64.ActiveCfg = Debug|arm64
{2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|arm64.Build.0 = Debug|arm64
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Debug|x86.Build.0 = Debug|Any CPU
{2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|x64.ActiveCfg = Release|x64
- {2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|arm64.ActiveCfg = Release|arm64
{2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|x64.Build.0 = Release|x64
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|arm64.ActiveCfg = Release|arm64
{2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|arm64.Build.0 = Release|arm64
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|x86.ActiveCfg = Release|Any CPU
+ {2979E556-5859-4E88-A1D4-EAB72F82294E}.Release|x86.Build.0 = Release|Any CPU
{7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|x64.ActiveCfg = Debug|x64
- {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|arm64.ActiveCfg = Debug|arm64
{7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|x64.Build.0 = Debug|x64
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|arm64.ActiveCfg = Debug|arm64
{7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|arm64.Build.0 = Debug|arm64
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Debug|x86.Build.0 = Debug|Any CPU
{7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|x64.ActiveCfg = Release|x64
- {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|arm64.ActiveCfg = Release|arm64
{7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|x64.Build.0 = Release|x64
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|arm64.ActiveCfg = Release|arm64
{7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|arm64.Build.0 = Release|arm64
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|x86.ActiveCfg = Release|Any CPU
+ {7E098666-DE8C-4ABF-B709-4CE7B1A491B0}.Release|x86.Build.0 = Release|Any CPU
{D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|x64.ActiveCfg = Debug|x64
- {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|arm64.ActiveCfg = Debug|arm64
{D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|x64.Build.0 = Debug|x64
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|arm64.ActiveCfg = Debug|arm64
{D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|arm64.Build.0 = Debug|arm64
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Debug|x86.Build.0 = Debug|Any CPU
{D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|x64.ActiveCfg = Release|x64
- {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|arm64.ActiveCfg = Release|arm64
{D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|x64.Build.0 = Release|x64
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|arm64.ActiveCfg = Release|arm64
{D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|arm64.Build.0 = Release|arm64
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|x86.ActiveCfg = Release|Any CPU
+ {D47CC16E-466B-4D58-A8FC-ECAE5C9606FC}.Release|x86.Build.0 = Release|Any CPU
{1143176D-B7F0-477C-90BB-72289068D927}.Debug|x64.ActiveCfg = Debug|x64
- {1143176D-B7F0-477C-90BB-72289068D927}.Debug|arm64.ActiveCfg = Debug|arm64
{1143176D-B7F0-477C-90BB-72289068D927}.Debug|x64.Build.0 = Debug|x64
+ {1143176D-B7F0-477C-90BB-72289068D927}.Debug|arm64.ActiveCfg = Debug|arm64
{1143176D-B7F0-477C-90BB-72289068D927}.Debug|arm64.Build.0 = Debug|arm64
+ {1143176D-B7F0-477C-90BB-72289068D927}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1143176D-B7F0-477C-90BB-72289068D927}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1143176D-B7F0-477C-90BB-72289068D927}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1143176D-B7F0-477C-90BB-72289068D927}.Debug|x86.Build.0 = Debug|Any CPU
{1143176D-B7F0-477C-90BB-72289068D927}.Release|x64.ActiveCfg = Release|x64
- {1143176D-B7F0-477C-90BB-72289068D927}.Release|arm64.ActiveCfg = Release|arm64
{1143176D-B7F0-477C-90BB-72289068D927}.Release|x64.Build.0 = Release|x64
+ {1143176D-B7F0-477C-90BB-72289068D927}.Release|arm64.ActiveCfg = Release|arm64
{1143176D-B7F0-477C-90BB-72289068D927}.Release|arm64.Build.0 = Release|arm64
+ {1143176D-B7F0-477C-90BB-72289068D927}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1143176D-B7F0-477C-90BB-72289068D927}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1143176D-B7F0-477C-90BB-72289068D927}.Release|x86.ActiveCfg = Release|Any CPU
+ {1143176D-B7F0-477C-90BB-72289068D927}.Release|x86.Build.0 = Release|Any CPU
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|x64.ActiveCfg = Debug|x64
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|x64.Build.0 = Debug|x64
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|arm64.ActiveCfg = Debug|arm64
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|arm64.Build.0 = Debug|arm64
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|x86.Build.0 = Debug|Any CPU
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|x64.ActiveCfg = Release|x64
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|x64.Build.0 = Release|x64
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|arm64.ActiveCfg = Release|arm64
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|arm64.Build.0 = Release|arm64
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|x86.ActiveCfg = Release|Any CPU
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|x86.Build.0 = Release|Any CPU
{E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|x64.ActiveCfg = Debug|x64
- {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|arm64.ActiveCfg = Debug|arm64
{E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|x64.Build.0 = Debug|x64
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|arm64.ActiveCfg = Debug|arm64
{E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|arm64.Build.0 = Debug|arm64
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Debug|x86.Build.0 = Debug|Any CPU
{E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|x64.ActiveCfg = Release|x64
- {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|arm64.ActiveCfg = Release|arm64
{E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|x64.Build.0 = Release|x64
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|arm64.ActiveCfg = Release|arm64
{E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|arm64.Build.0 = Release|arm64
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|x86.ActiveCfg = Release|Any CPU
+ {E454D3A5-C5C6-4291-BE96-220CF0D5CFFD}.Release|x86.Build.0 = Release|Any CPU
{57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|x64.ActiveCfg = Debug|x64
- {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|arm64.ActiveCfg = Debug|arm64
{57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|x64.Build.0 = Debug|x64
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|arm64.ActiveCfg = Debug|arm64
{57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|arm64.Build.0 = Debug|arm64
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Debug|x86.Build.0 = Debug|Any CPU
{57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|x64.ActiveCfg = Release|x64
- {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|arm64.ActiveCfg = Release|arm64
{57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|x64.Build.0 = Release|x64
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|arm64.ActiveCfg = Release|arm64
{57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|arm64.Build.0 = Release|arm64
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|x86.ActiveCfg = Release|Any CPU
+ {57D094C1-6913-46BF-A657-84A5F46D4EE7}.Release|x86.Build.0 = Release|Any CPU
{740E2894-903D-4B94-9C32-B630593BEB16}.Debug|x64.ActiveCfg = Debug|x64
- {740E2894-903D-4B94-9C32-B630593BEB16}.Debug|arm64.ActiveCfg = Debug|arm64
{740E2894-903D-4B94-9C32-B630593BEB16}.Debug|x64.Build.0 = Debug|x64
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Debug|arm64.ActiveCfg = Debug|arm64
{740E2894-903D-4B94-9C32-B630593BEB16}.Debug|arm64.Build.0 = Debug|arm64
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Debug|x86.Build.0 = Debug|Any CPU
{740E2894-903D-4B94-9C32-B630593BEB16}.Release|x64.ActiveCfg = Release|x64
- {740E2894-903D-4B94-9C32-B630593BEB16}.Release|arm64.ActiveCfg = Release|arm64
{740E2894-903D-4B94-9C32-B630593BEB16}.Release|x64.Build.0 = Release|x64
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Release|arm64.ActiveCfg = Release|arm64
{740E2894-903D-4B94-9C32-B630593BEB16}.Release|arm64.Build.0 = Release|arm64
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Release|Any CPU.Build.0 = Release|Any CPU
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Release|x86.ActiveCfg = Release|Any CPU
+ {740E2894-903D-4B94-9C32-B630593BEB16}.Release|x86.Build.0 = Release|Any CPU
{D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|x64.ActiveCfg = Debug|x64
- {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|arm64.ActiveCfg = Debug|arm64
{D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|x64.Build.0 = Debug|x64
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|arm64.ActiveCfg = Debug|arm64
{D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|arm64.Build.0 = Debug|arm64
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Debug|x86.Build.0 = Debug|Any CPU
{D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|x64.ActiveCfg = Release|x64
- {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|arm64.ActiveCfg = Release|arm64
{D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|x64.Build.0 = Release|x64
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|arm64.ActiveCfg = Release|arm64
{D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|arm64.Build.0 = Release|arm64
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|x86.ActiveCfg = Release|Any CPU
+ {D401F706-A182-46E3-A25C-B0BF5AA0D07E}.Release|x86.Build.0 = Release|Any CPU
{0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|x64.ActiveCfg = Debug|x64
- {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|arm64.ActiveCfg = Debug|arm64
{0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|x64.Build.0 = Debug|x64
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|arm64.ActiveCfg = Debug|arm64
{0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|arm64.Build.0 = Debug|arm64
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Debug|x86.Build.0 = Debug|Any CPU
{0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|x64.ActiveCfg = Release|x64
- {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|arm64.ActiveCfg = Release|arm64
{0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|x64.Build.0 = Release|x64
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|arm64.ActiveCfg = Release|arm64
{0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|arm64.Build.0 = Release|arm64
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|x86.ActiveCfg = Release|Any CPU
+ {0FFA3F96-A68A-453F-A5FE-0C281EC371C7}.Release|x86.Build.0 = Release|Any CPU
{5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|x64.ActiveCfg = Debug|x64
- {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|arm64.ActiveCfg = Debug|arm64
{5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|x64.Build.0 = Debug|x64
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|arm64.ActiveCfg = Debug|arm64
{5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|arm64.Build.0 = Debug|arm64
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Debug|x86.Build.0 = Debug|Any CPU
{5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|x64.ActiveCfg = Release|x64
- {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|arm64.ActiveCfg = Release|arm64
{5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|x64.Build.0 = Release|x64
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|arm64.ActiveCfg = Release|arm64
{5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|arm64.Build.0 = Release|arm64
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|x86.ActiveCfg = Release|Any CPU
+ {5FA79592-DE5B-46FF-9E05-34A2E72A7AF7}.Release|x86.Build.0 = Release|Any CPU
{09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|x64.ActiveCfg = Debug|x64
- {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|arm64.ActiveCfg = Debug|arm64
{09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|x64.Build.0 = Debug|x64
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|arm64.ActiveCfg = Debug|arm64
{09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|arm64.Build.0 = Debug|arm64
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Debug|x86.Build.0 = Debug|Any CPU
{09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|x64.ActiveCfg = Release|x64
- {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|arm64.ActiveCfg = Release|arm64
{09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|x64.Build.0 = Release|x64
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|arm64.ActiveCfg = Release|arm64
{09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|arm64.Build.0 = Release|arm64
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|x86.ActiveCfg = Release|Any CPU
+ {09FD3D3A-1EFC-4AEE-B3D7-096D238E0D1A}.Release|x86.Build.0 = Release|Any CPU
{BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|x64.ActiveCfg = Debug|x64
- {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|arm64.ActiveCfg = Debug|arm64
{BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|x64.Build.0 = Debug|x64
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|arm64.ActiveCfg = Debug|arm64
{BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|arm64.Build.0 = Debug|arm64
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Debug|x86.Build.0 = Debug|Any CPU
{BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|x64.ActiveCfg = Release|x64
- {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|arm64.ActiveCfg = Release|arm64
{BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|x64.Build.0 = Release|x64
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|arm64.ActiveCfg = Release|arm64
{BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|arm64.Build.0 = Release|arm64
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|x86.ActiveCfg = Release|Any CPU
+ {BDB7A8F3-87A6-4B77-9E0F-6BC785CBCF2B}.Release|x86.Build.0 = Release|Any CPU
{27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|x64.ActiveCfg = Debug|x64
- {27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|arm64.ActiveCfg = Debug|arm64
{27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|x64.Build.0 = Debug|x64
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|arm64.ActiveCfg = Debug|arm64
{27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|arm64.Build.0 = Debug|arm64
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Debug|x86.Build.0 = Debug|Any CPU
{27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|x64.ActiveCfg = Release|x64
- {27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|arm64.ActiveCfg = Release|arm64
{27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|x64.Build.0 = Release|x64
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|arm64.ActiveCfg = Release|arm64
{27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|arm64.Build.0 = Release|arm64
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|x86.ActiveCfg = Release|Any CPU
+ {27E0B288-7DFF-468D-9360-035E8CE123CB}.Release|x86.Build.0 = Release|Any CPU
{20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|x64.ActiveCfg = Debug|x64
- {20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|arm64.ActiveCfg = Debug|arm64
{20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|x64.Build.0 = Debug|x64
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|arm64.ActiveCfg = Debug|arm64
{20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|arm64.Build.0 = Debug|arm64
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Debug|x86.Build.0 = Debug|Any CPU
{20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|x64.ActiveCfg = Release|x64
- {20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|arm64.ActiveCfg = Release|arm64
{20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|x64.Build.0 = Release|x64
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|arm64.ActiveCfg = Release|arm64
{20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|arm64.Build.0 = Release|arm64
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|x86.ActiveCfg = Release|Any CPU
+ {20F43119-EA00-440D-B3B5-44F96592F4C8}.Release|x86.Build.0 = Release|Any CPU
{CC400751-216E-4BB2-8103-495B4E273477}.Debug|x64.ActiveCfg = Debug|x64
- {CC400751-216E-4BB2-8103-495B4E273477}.Debug|arm64.ActiveCfg = Debug|arm64
{CC400751-216E-4BB2-8103-495B4E273477}.Debug|x64.Build.0 = Debug|x64
+ {CC400751-216E-4BB2-8103-495B4E273477}.Debug|arm64.ActiveCfg = Debug|arm64
{CC400751-216E-4BB2-8103-495B4E273477}.Debug|arm64.Build.0 = Debug|arm64
+ {CC400751-216E-4BB2-8103-495B4E273477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC400751-216E-4BB2-8103-495B4E273477}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC400751-216E-4BB2-8103-495B4E273477}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CC400751-216E-4BB2-8103-495B4E273477}.Debug|x86.Build.0 = Debug|Any CPU
{CC400751-216E-4BB2-8103-495B4E273477}.Release|x64.ActiveCfg = Release|x64
- {CC400751-216E-4BB2-8103-495B4E273477}.Release|arm64.ActiveCfg = Release|arm64
{CC400751-216E-4BB2-8103-495B4E273477}.Release|x64.Build.0 = Release|x64
+ {CC400751-216E-4BB2-8103-495B4E273477}.Release|arm64.ActiveCfg = Release|arm64
{CC400751-216E-4BB2-8103-495B4E273477}.Release|arm64.Build.0 = Release|arm64
+ {CC400751-216E-4BB2-8103-495B4E273477}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC400751-216E-4BB2-8103-495B4E273477}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC400751-216E-4BB2-8103-495B4E273477}.Release|x86.ActiveCfg = Release|Any CPU
+ {CC400751-216E-4BB2-8103-495B4E273477}.Release|x86.Build.0 = Release|Any CPU
{F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|x64.ActiveCfg = Debug|x64
- {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|arm64.ActiveCfg = Debug|arm64
{F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|x64.Build.0 = Debug|x64
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|arm64.ActiveCfg = Debug|arm64
{F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|arm64.Build.0 = Debug|arm64
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Debug|x86.Build.0 = Debug|Any CPU
{F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|x64.ActiveCfg = Release|x64
- {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|arm64.ActiveCfg = Release|arm64
{F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|x64.Build.0 = Release|x64
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|arm64.ActiveCfg = Release|arm64
{F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|arm64.Build.0 = Release|arm64
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|x86.ActiveCfg = Release|Any CPU
+ {F4E7301D-9C8A-4A4A-92D2-35B896642BF6}.Release|x86.Build.0 = Release|Any CPU
{29450002-5F93-4886-922E-30350C9C3442}.Debug|x64.ActiveCfg = Debug|x64
- {29450002-5F93-4886-922E-30350C9C3442}.Debug|arm64.ActiveCfg = Debug|arm64
{29450002-5F93-4886-922E-30350C9C3442}.Debug|x64.Build.0 = Debug|x64
+ {29450002-5F93-4886-922E-30350C9C3442}.Debug|arm64.ActiveCfg = Debug|arm64
{29450002-5F93-4886-922E-30350C9C3442}.Debug|arm64.Build.0 = Debug|arm64
+ {29450002-5F93-4886-922E-30350C9C3442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {29450002-5F93-4886-922E-30350C9C3442}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {29450002-5F93-4886-922E-30350C9C3442}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {29450002-5F93-4886-922E-30350C9C3442}.Debug|x86.Build.0 = Debug|Any CPU
{29450002-5F93-4886-922E-30350C9C3442}.Release|x64.ActiveCfg = Release|x64
- {29450002-5F93-4886-922E-30350C9C3442}.Release|arm64.ActiveCfg = Release|arm64
{29450002-5F93-4886-922E-30350C9C3442}.Release|x64.Build.0 = Release|x64
+ {29450002-5F93-4886-922E-30350C9C3442}.Release|arm64.ActiveCfg = Release|arm64
{29450002-5F93-4886-922E-30350C9C3442}.Release|arm64.Build.0 = Release|arm64
+ {29450002-5F93-4886-922E-30350C9C3442}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {29450002-5F93-4886-922E-30350C9C3442}.Release|Any CPU.Build.0 = Release|Any CPU
+ {29450002-5F93-4886-922E-30350C9C3442}.Release|x86.ActiveCfg = Release|Any CPU
+ {29450002-5F93-4886-922E-30350C9C3442}.Release|x86.Build.0 = Release|Any CPU
{C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|x64.ActiveCfg = Debug|x64
- {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|arm64.ActiveCfg = Debug|arm64
{C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|x64.Build.0 = Debug|x64
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|arm64.ActiveCfg = Debug|arm64
{C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|arm64.Build.0 = Debug|arm64
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Debug|x86.Build.0 = Debug|Any CPU
{C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|x64.ActiveCfg = Release|x64
- {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|arm64.ActiveCfg = Release|arm64
{C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|x64.Build.0 = Release|x64
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|arm64.ActiveCfg = Release|arm64
{C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|arm64.Build.0 = Release|arm64
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|x86.ActiveCfg = Release|Any CPU
+ {C396E5F6-C6D9-465D-9903-7E33D0841E6A}.Release|x86.Build.0 = Release|Any CPU
{54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|x64.ActiveCfg = Debug|x64
- {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|arm64.ActiveCfg = Debug|arm64
{54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|x64.Build.0 = Debug|x64
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|arm64.ActiveCfg = Debug|arm64
{54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|arm64.Build.0 = Debug|arm64
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Debug|x86.Build.0 = Debug|Any CPU
{54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|x64.ActiveCfg = Release|x64
- {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|arm64.ActiveCfg = Release|arm64
{54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|x64.Build.0 = Release|x64
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|arm64.ActiveCfg = Release|arm64
{54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|arm64.Build.0 = Release|arm64
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|x86.ActiveCfg = Release|Any CPU
+ {54DA0549-366F-4E70-B5D1-0B8891D0A2A5}.Release|x86.Build.0 = Release|Any CPU
{E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|x64.ActiveCfg = Debug|x64
- {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|arm64.ActiveCfg = Debug|arm64
{E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|x64.Build.0 = Debug|x64
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|arm64.ActiveCfg = Debug|arm64
{E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|arm64.Build.0 = Debug|arm64
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Debug|x86.Build.0 = Debug|Any CPU
{E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|x64.ActiveCfg = Release|x64
- {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|arm64.ActiveCfg = Release|arm64
{E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|x64.Build.0 = Release|x64
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|arm64.ActiveCfg = Release|arm64
{E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|arm64.Build.0 = Release|arm64
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|x86.ActiveCfg = Release|Any CPU
+ {E337A71E-3C30-4315-B8F1-57CBC5CF50A6}.Release|x86.Build.0 = Release|Any CPU
{727866B8-BBD5-43B9-933A-78199F65429C}.Debug|x64.ActiveCfg = Debug|x64
- {727866B8-BBD5-43B9-933A-78199F65429C}.Debug|arm64.ActiveCfg = Debug|arm64
{727866B8-BBD5-43B9-933A-78199F65429C}.Debug|x64.Build.0 = Debug|x64
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Debug|arm64.ActiveCfg = Debug|arm64
{727866B8-BBD5-43B9-933A-78199F65429C}.Debug|arm64.Build.0 = Debug|arm64
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Debug|x86.Build.0 = Debug|Any CPU
{727866B8-BBD5-43B9-933A-78199F65429C}.Release|x64.ActiveCfg = Release|x64
- {727866B8-BBD5-43B9-933A-78199F65429C}.Release|arm64.ActiveCfg = Release|arm64
{727866B8-BBD5-43B9-933A-78199F65429C}.Release|x64.Build.0 = Release|x64
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Release|arm64.ActiveCfg = Release|arm64
{727866B8-BBD5-43B9-933A-78199F65429C}.Release|arm64.Build.0 = Release|arm64
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Release|x86.ActiveCfg = Release|Any CPU
+ {727866B8-BBD5-43B9-933A-78199F65429C}.Release|x86.Build.0 = Release|Any CPU
{3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|x64.ActiveCfg = Debug|x64
- {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|arm64.ActiveCfg = Debug|arm64
{3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|x64.Build.0 = Debug|x64
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|arm64.ActiveCfg = Debug|arm64
{3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|arm64.Build.0 = Debug|arm64
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Debug|x86.Build.0 = Debug|Any CPU
{3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|x64.ActiveCfg = Release|x64
- {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|arm64.ActiveCfg = Release|arm64
{3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|x64.Build.0 = Release|x64
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|arm64.ActiveCfg = Release|arm64
{3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|arm64.Build.0 = Release|arm64
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|x86.ActiveCfg = Release|Any CPU
+ {3C8BF564-B4B5-44A7-9D8C-102C2F820EAF}.Release|x86.Build.0 = Release|Any CPU
{F1610A61-5444-4C11-9447-13CCA327887E}.Debug|x64.ActiveCfg = Debug|x64
- {F1610A61-5444-4C11-9447-13CCA327887E}.Debug|arm64.ActiveCfg = Debug|arm64
{F1610A61-5444-4C11-9447-13CCA327887E}.Debug|x64.Build.0 = Debug|x64
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Debug|arm64.ActiveCfg = Debug|arm64
{F1610A61-5444-4C11-9447-13CCA327887E}.Debug|arm64.Build.0 = Debug|arm64
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Debug|x86.Build.0 = Debug|Any CPU
{F1610A61-5444-4C11-9447-13CCA327887E}.Release|x64.ActiveCfg = Release|x64
- {F1610A61-5444-4C11-9447-13CCA327887E}.Release|arm64.ActiveCfg = Release|arm64
{F1610A61-5444-4C11-9447-13CCA327887E}.Release|x64.Build.0 = Release|x64
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Release|arm64.ActiveCfg = Release|arm64
{F1610A61-5444-4C11-9447-13CCA327887E}.Release|arm64.Build.0 = Release|arm64
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Release|x86.ActiveCfg = Release|Any CPU
+ {F1610A61-5444-4C11-9447-13CCA327887E}.Release|x86.Build.0 = Release|Any CPU
{B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|x64.ActiveCfg = Debug|x64
- {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|arm64.ActiveCfg = Debug|arm64
{B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|x64.Build.0 = Debug|x64
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|arm64.ActiveCfg = Debug|arm64
{B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|arm64.Build.0 = Debug|arm64
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Debug|x86.Build.0 = Debug|Any CPU
{B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|x64.ActiveCfg = Release|x64
- {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|arm64.ActiveCfg = Release|arm64
{B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|x64.Build.0 = Release|x64
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|arm64.ActiveCfg = Release|arm64
{B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|arm64.Build.0 = Release|arm64
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|x64.ActiveCfg = Debug|x64
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|arm64.ActiveCfg = Debug|arm64
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|x64.Build.0 = Debug|x64
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Debug|arm64.Build.0 = Debug|arm64
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|x64.ActiveCfg = Release|x64
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|arm64.ActiveCfg = Release|arm64
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|x64.Build.0 = Release|x64
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6}.Release|arm64.Build.0 = Release|arm64
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|x86.ActiveCfg = Release|Any CPU
+ {B0E59327-933E-4DB0-BD2D-FB16EB9B4194}.Release|x86.Build.0 = Release|Any CPU
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Debug|x64.ActiveCfg = Debug|x64
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Debug|x64.Build.0 = Debug|x64
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Debug|arm64.ActiveCfg = Debug|arm64
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Debug|arm64.Build.0 = Debug|arm64
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Debug|x86.Build.0 = Debug|Any CPU
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Release|x64.ActiveCfg = Release|x64
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Release|x64.Build.0 = Release|x64
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Release|arm64.ActiveCfg = Release|arm64
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Release|arm64.Build.0 = Release|arm64
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Release|Any CPU.Build.0 = Release|Any CPU
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Release|x86.ActiveCfg = Release|Any CPU
+ {181E4491-B96D-4E33-8102-AC91420D6123}.Release|x86.Build.0 = Release|Any CPU
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Debug|x64.ActiveCfg = Debug|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Debug|x64.Build.0 = Debug|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Debug|arm64.ActiveCfg = Debug|arm64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Debug|arm64.Build.0 = Debug|arm64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Debug|Any CPU.Build.0 = Debug|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Debug|x86.ActiveCfg = Debug|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Debug|x86.Build.0 = Debug|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Release|x64.ActiveCfg = Release|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Release|x64.Build.0 = Release|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Release|arm64.ActiveCfg = Release|arm64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Release|arm64.Build.0 = Release|arm64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Release|Any CPU.ActiveCfg = Release|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Release|Any CPU.Build.0 = Release|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Release|x86.ActiveCfg = Release|x64
+ {991DBBC6-3E13-4A95-BCDC-F99512F87DC3}.Release|x86.Build.0 = Release|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Debug|x64.ActiveCfg = Debug|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Debug|x64.Build.0 = Debug|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Debug|arm64.ActiveCfg = Debug|arm64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Debug|arm64.Build.0 = Debug|arm64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Debug|Any CPU.Build.0 = Debug|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Debug|x86.ActiveCfg = Debug|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Debug|x86.Build.0 = Debug|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Release|x64.ActiveCfg = Release|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Release|x64.Build.0 = Release|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Release|arm64.ActiveCfg = Release|arm64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Release|arm64.Build.0 = Release|arm64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Release|Any CPU.ActiveCfg = Release|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Release|Any CPU.Build.0 = Release|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Release|x86.ActiveCfg = Release|x64
+ {8CDDF549-38D0-45DB-9494-3B9B3376E7C0}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -480,6 +876,7 @@ Global
{95168D0B-1B2C-4295-B6D4-D5BAF781B9FA} = {7940E867-EEBA-4AFD-9904-1536F003239C}
{D47CC16E-466B-4D58-A8FC-ECAE5C9606FC} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA}
{1143176D-B7F0-477C-90BB-72289068D927} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA}
+ {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6} = {7940E867-EEBA-4AFD-9904-1536F003239C}
{E454D3A5-C5C6-4291-BE96-220CF0D5CFFD} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA}
{57D094C1-6913-46BF-A657-84A5F46D4EE7} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA}
{740E2894-903D-4B94-9C32-B630593BEB16} = {95168D0B-1B2C-4295-B6D4-D5BAF781B9FA}
@@ -502,7 +899,7 @@ Global
{015B44EE-32AE-4105-9016-49140743CAF9} = {7940E867-EEBA-4AFD-9904-1536F003239C}
{F1610A61-5444-4C11-9447-13CCA327887E} = {015B44EE-32AE-4105-9016-49140743CAF9}
{B0E59327-933E-4DB0-BD2D-FB16EB9B4194} = {E05D1183-D360-4AFE-8968-314A34FAD3B2}
- {A8B3C2D1-5E4F-4A7B-9C0D-E1F2A3B4C5D6} = {7940E867-EEBA-4AFD-9904-1536F003239C}
+ {181E4491-B96D-4E33-8102-AC91420D6123} = {015B44EE-32AE-4105-9016-49140743CAF9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D044BB14-0B37-47E5-A579-8B30FCBA1F9F}
diff --git a/src/UniGetUI/AutoUpdater.Helpers.cs b/src/UniGetUI/AutoUpdater.Helpers.cs
new file mode 100644
index 0000000000..35715f0c38
--- /dev/null
+++ b/src/UniGetUI/AutoUpdater.Helpers.cs
@@ -0,0 +1,281 @@
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Win32;
+using UniGetUI.Core.Logging;
+using UniGetUI.Core.Tools;
+
+namespace UniGetUI;
+
+public partial class AutoUpdater
+{
+ private const string REGISTRY_PATH = @"Software\Devolutions\UniGetUI";
+ private const string DEFAULT_PRODUCTINFO_URL = "https://devolutions.net/productinfo.json";
+ private const string DEFAULT_PRODUCTINFO_KEY = "Devolutions.UniGetUI";
+
+ private const string REG_PRODUCTINFO_URL = "UpdaterProductInfoUrl";
+ private const string REG_PRODUCTINFO_KEY = "UpdaterProductKey";
+ private const string REG_ALLOW_UNSAFE_URLS = "UpdaterAllowUnsafeUrls";
+ private const string REG_SKIP_HASH_VALIDATION = "UpdaterSkipHashValidation";
+ private const string REG_SKIP_SIGNER_THUMBPRINT_CHECK = "UpdaterSkipSignerThumbprintCheck";
+ private const string REG_DISABLE_TLS_VALIDATION = "UpdaterDisableTlsValidation";
+
+#if !DEBUG
+ private static readonly string[] RELEASE_IGNORED_REGISTRY_VALUES =
+ [
+ REG_PRODUCTINFO_KEY,
+ REG_ALLOW_UNSAFE_URLS,
+ REG_SKIP_HASH_VALIDATION,
+ REG_SKIP_SIGNER_THUMBPRINT_CHECK,
+ REG_DISABLE_TLS_VALIDATION,
+ ];
+#endif
+
+ private static HttpClientHandler CreateHttpClientHandler(UpdaterOverrides updaterOverrides)
+ {
+ HttpClientHandler handler = CoreTools.GenericHttpClientParameters;
+ if (updaterOverrides.DisableTlsValidation)
+ {
+ Logger.Warn(
+ "Registry override enabled: TLS certificate validation is disabled for updater requests."
+ );
+ handler.ServerCertificateCustomValidationCallback = static (_, _, _, _) => true;
+ }
+
+ return handler;
+ }
+
+ internal static bool IsSourceUrlAllowed(string url, bool allowUnsafeUrls)
+ {
+ if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri))
+ {
+ return false;
+ }
+
+ if (allowUnsafeUrls)
+ {
+ Logger.Warn($"Registry override enabled: allowing potentially unsafe updater URL {url}");
+ return true;
+ }
+
+ if (!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ return uri.Host.Equals("devolutions.net", StringComparison.OrdinalIgnoreCase)
+ || uri.Host.EndsWith(".devolutions.net", StringComparison.OrdinalIgnoreCase)
+ || uri.Host.Equals("github.com", StringComparison.OrdinalIgnoreCase)
+ || uri.Host.Equals("objects.githubusercontent.com", StringComparison.OrdinalIgnoreCase)
+ || uri.Host.Equals(
+ "release-assets.githubusercontent.com",
+ StringComparison.OrdinalIgnoreCase
+ );
+ }
+
+ internal static ProductInfoFile SelectInstallerFile(List files)
+ {
+ string targetArch = RuntimeInformation.ProcessArchitecture switch
+ {
+ Architecture.Arm64 => "arm64",
+ Architecture.X64 => "x64",
+ _ => "x64",
+ };
+
+ ProductInfoFile? match = files.FirstOrDefault(file =>
+ file.Type.Equals("exe", StringComparison.OrdinalIgnoreCase)
+ && file.Arch.Equals(targetArch, StringComparison.OrdinalIgnoreCase)
+ );
+
+ match ??= files.FirstOrDefault(file =>
+ file.Type.Equals("exe", StringComparison.OrdinalIgnoreCase)
+ && file.Arch.Equals("Any", StringComparison.OrdinalIgnoreCase)
+ );
+
+ match ??= files.FirstOrDefault(file =>
+ file.Type.Equals("msi", StringComparison.OrdinalIgnoreCase)
+ && file.Arch.Equals(targetArch, StringComparison.OrdinalIgnoreCase)
+ );
+
+ match ??= files.FirstOrDefault(file =>
+ file.Type.Equals("msi", StringComparison.OrdinalIgnoreCase)
+ && file.Arch.Equals("Any", StringComparison.OrdinalIgnoreCase)
+ );
+
+ if (match is null)
+ {
+ throw new KeyNotFoundException(
+ $"No compatible installer file found in productinfo for architecture '{targetArch}'"
+ );
+ }
+
+ return match;
+ }
+
+ internal static Version ParseVersionOrFallback(string rawVersion, Version fallbackVersion)
+ {
+ if (Version.TryParse(rawVersion, out Version? parsed))
+ {
+ return CoreTools.NormalizeVersionForComparison(parsed);
+ }
+
+ string sanitized = rawVersion.Trim().TrimStart('v', 'V');
+ if (Version.TryParse(sanitized, out parsed))
+ {
+ return CoreTools.NormalizeVersionForComparison(parsed);
+ }
+
+ Logger.Warn($"Could not parse version '{rawVersion}', using fallback '{fallbackVersion}'");
+ return fallbackVersion;
+ }
+
+ internal static string NormalizeThumbprint(string thumbprint)
+ {
+ char[] normalized = thumbprint.ToLowerInvariant().Where(char.IsAsciiHexDigit).ToArray();
+
+ return new string(normalized);
+ }
+
+ private static UpdaterOverrides LoadUpdaterOverrides()
+ {
+ using RegistryKey? key = Registry.LocalMachine.OpenSubKey(REGISTRY_PATH);
+
+#if DEBUG
+ if (key is not null)
+ {
+ Logger.Info($"Updater registry overrides loaded from HKLM\\{REGISTRY_PATH}");
+ }
+
+ return new UpdaterOverrides(
+ GetRegistryString(key, REG_PRODUCTINFO_URL) ?? DEFAULT_PRODUCTINFO_URL,
+ GetRegistryString(key, REG_PRODUCTINFO_KEY) ?? DEFAULT_PRODUCTINFO_KEY,
+ GetRegistryBool(key, REG_ALLOW_UNSAFE_URLS),
+ GetRegistryBool(key, REG_SKIP_HASH_VALIDATION),
+ GetRegistryBool(key, REG_SKIP_SIGNER_THUMBPRINT_CHECK),
+ GetRegistryBool(key, REG_DISABLE_TLS_VALIDATION)
+ );
+#else
+ LogIgnoredReleaseOverrides(key);
+ string productInfoUrl =
+ GetRegistryString(key, REG_PRODUCTINFO_URL) ?? DEFAULT_PRODUCTINFO_URL;
+
+ return new UpdaterOverrides(
+ productInfoUrl,
+ DEFAULT_PRODUCTINFO_KEY,
+ false,
+ false,
+ false,
+ false
+ );
+#endif
+ }
+
+#if !DEBUG
+ private static void LogIgnoredReleaseOverrides(RegistryKey? key)
+ {
+ if (key is null)
+ {
+ return;
+ }
+
+ foreach (string valueName in RELEASE_IGNORED_REGISTRY_VALUES)
+ {
+ if (key.GetValue(valueName) is not null)
+ {
+ Logger.Warn(
+ $"Release build is ignoring updater registry value HKLM\\{REGISTRY_PATH}\\{valueName}."
+ );
+ }
+ }
+ }
+#endif
+
+ internal static string? GetRegistryString(RegistryKey? key, string valueName)
+ {
+#pragma warning disable CA1416
+ object? value = key?.GetValue(valueName);
+#pragma warning restore CA1416
+ if (value is null)
+ {
+ return null;
+ }
+
+ string? parsedValue = value.ToString();
+ if (string.IsNullOrWhiteSpace(parsedValue))
+ {
+ return null;
+ }
+
+ return parsedValue.Trim();
+ }
+
+#if DEBUG
+ internal static bool GetRegistryBool(RegistryKey? key, string valueName)
+ {
+#pragma warning disable CA1416
+ object? value = key?.GetValue(valueName);
+#pragma warning restore CA1416
+ if (value is null)
+ {
+ return false;
+ }
+
+ if (value is int intValue)
+ {
+ return intValue != 0;
+ }
+
+ if (value is long longValue)
+ {
+ return longValue != 0;
+ }
+
+ string normalized = value.ToString()?.Trim() ?? "";
+ return normalized.Equals("1", StringComparison.OrdinalIgnoreCase)
+ || normalized.Equals("true", StringComparison.OrdinalIgnoreCase)
+ || normalized.Equals("yes", StringComparison.OrdinalIgnoreCase)
+ || normalized.Equals("on", StringComparison.OrdinalIgnoreCase);
+ }
+#endif
+
+ private sealed record UpdateCandidate(
+ bool IsUpgradable,
+ string VersionName,
+ string InstallerHash,
+ string InstallerDownloadUrl,
+ string SourceName
+ );
+
+ private sealed record UpdaterOverrides(
+ string ProductInfoUrl,
+ string ProductInfoProductKey,
+ bool AllowUnsafeUrls,
+ bool SkipHashValidation,
+ bool SkipSignerThumbprintCheck,
+ bool DisableTlsValidation
+ );
+
+ private sealed class ProductInfoProduct
+ {
+ public ProductInfoChannel? Current { get; set; }
+ public ProductInfoChannel? Beta { get; set; }
+ }
+
+ internal sealed class ProductInfoChannel
+ {
+ public string Version { get; set; } = string.Empty;
+ public List Files { get; set; } = [];
+ }
+
+ internal sealed class ProductInfoFile
+ {
+ public string Arch { get; set; } = string.Empty;
+ public string Type { get; set; } = string.Empty;
+ public string Url { get; set; } = string.Empty;
+ public string Hash { get; set; } = string.Empty;
+ }
+
+ [JsonSourceGenerationOptions(AllowTrailingCommas = true)]
+ [JsonSerializable(typeof(Dictionary))]
+ private sealed partial class AutoUpdaterJsonContext : JsonSerializerContext { }
+}
diff --git a/src/UniGetUI/AutoUpdater.cs b/src/UniGetUI/AutoUpdater.cs
index c2204d4cea..f5b58d8430 100644
--- a/src/UniGetUI/AutoUpdater.cs
+++ b/src/UniGetUI/AutoUpdater.cs
@@ -1,13 +1,9 @@
using System.Diagnostics;
using System.Globalization;
-using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
-using Microsoft.Win32;
using Microsoft.Windows.AppNotifications;
using Microsoft.Windows.AppNotifications.Builder;
using UniGetUI.Core.Data;
@@ -20,17 +16,6 @@ namespace UniGetUI;
public partial class AutoUpdater
{
- private const string REGISTRY_PATH = @"Software\Devolutions\UniGetUI";
- private const string DEFAULT_PRODUCTINFO_URL = "https://devolutions.net/productinfo.json";
- private const string DEFAULT_PRODUCTINFO_KEY = "Devolutions.UniGetUI";
-
- private const string REG_PRODUCTINFO_URL = "UpdaterProductInfoUrl";
- private const string REG_PRODUCTINFO_KEY = "UpdaterProductKey";
- private const string REG_ALLOW_UNSAFE_URLS = "UpdaterAllowUnsafeUrls";
- private const string REG_SKIP_HASH_VALIDATION = "UpdaterSkipHashValidation";
- private const string REG_SKIP_SIGNER_THUMBPRINT_CHECK = "UpdaterSkipSignerThumbprintCheck";
- private const string REG_DISABLE_TLS_VALIDATION = "UpdaterDisableTlsValidation";
-
private static readonly string[] DEVOLUTIONS_CERT_THUMBPRINTS =
[
"3f5202a9432d54293bdfe6f7e46adb0a6f8b3ba6",
@@ -38,17 +23,6 @@ public partial class AutoUpdater
"50f753333811ff11f1920274afde3ffd4468b210",
];
-#if !DEBUG
- private static readonly string[] RELEASE_IGNORED_REGISTRY_VALUES =
- [
- REG_PRODUCTINFO_KEY,
- REG_ALLOW_UNSAFE_URLS,
- REG_SKIP_HASH_VALIDATION,
- REG_SKIP_SIGNER_THUMBPRINT_CHECK,
- REG_DISABLE_TLS_VALIDATION,
- ];
-#endif
-
private static readonly AutoUpdaterJsonContext ProductInfoJsonContext = new(
new JsonSerializerOptions(SerializationHelpers.DefaultOptions)
);
@@ -594,252 +568,4 @@ private static void ShowMessage_ThreadSafe(
}
}
- private static HttpClientHandler CreateHttpClientHandler(UpdaterOverrides updaterOverrides)
- {
- HttpClientHandler handler = CoreTools.GenericHttpClientParameters;
- if (updaterOverrides.DisableTlsValidation)
- {
- Logger.Warn(
- "Registry override enabled: TLS certificate validation is disabled for updater requests."
- );
- handler.ServerCertificateCustomValidationCallback = static (_, _, _, _) => true;
- }
-
- return handler;
- }
-
- private static bool IsSourceUrlAllowed(string url, bool allowUnsafeUrls)
- {
- if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri))
- {
- return false;
- }
-
- if (allowUnsafeUrls)
- {
- Logger.Warn(
- $"Registry override enabled: allowing potentially unsafe updater URL {url}"
- );
- return true;
- }
-
- if (!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- return uri.Host.EndsWith("devolutions.net", StringComparison.OrdinalIgnoreCase)
- || uri.Host.Equals("github.com", StringComparison.OrdinalIgnoreCase)
- || uri.Host.Equals("objects.githubusercontent.com", StringComparison.OrdinalIgnoreCase)
- || uri.Host.Equals(
- "release-assets.githubusercontent.com",
- StringComparison.OrdinalIgnoreCase
- );
- }
-
- private static ProductInfoFile SelectInstallerFile(List files)
- {
- string targetArch = RuntimeInformation.ProcessArchitecture switch
- {
- Architecture.Arm64 => "arm64",
- Architecture.X64 => "x64",
- _ => "x64",
- };
-
- ProductInfoFile? match = files.FirstOrDefault(file =>
- file.Type.Equals("exe", StringComparison.OrdinalIgnoreCase)
- && file.Arch.Equals(targetArch, StringComparison.OrdinalIgnoreCase)
- );
-
- match ??= files.FirstOrDefault(file =>
- file.Type.Equals("exe", StringComparison.OrdinalIgnoreCase)
- && file.Arch.Equals("Any", StringComparison.OrdinalIgnoreCase)
- );
-
- match ??= files.FirstOrDefault(file =>
- file.Type.Equals("msi", StringComparison.OrdinalIgnoreCase)
- && file.Arch.Equals(targetArch, StringComparison.OrdinalIgnoreCase)
- );
-
- match ??= files.FirstOrDefault(file =>
- file.Type.Equals("msi", StringComparison.OrdinalIgnoreCase)
- && file.Arch.Equals("Any", StringComparison.OrdinalIgnoreCase)
- );
-
- if (match is null)
- {
- throw new KeyNotFoundException(
- $"No compatible installer file found in productinfo for architecture '{targetArch}'"
- );
- }
-
- return match;
- }
-
- private static Version ParseVersionOrFallback(string rawVersion, Version fallbackVersion)
- {
- if (Version.TryParse(rawVersion, out Version? parsed))
- {
- return CoreTools.NormalizeVersionForComparison(parsed);
- }
-
- string sanitized = rawVersion.Trim().TrimStart('v', 'V');
- if (Version.TryParse(sanitized, out parsed))
- {
- return CoreTools.NormalizeVersionForComparison(parsed);
- }
-
- Logger.Warn($"Could not parse version '{rawVersion}', using fallback '{fallbackVersion}'");
- return fallbackVersion;
- }
-
- private static string NormalizeThumbprint(string thumbprint)
- {
- char[] normalized = thumbprint.ToLowerInvariant().Where(char.IsAsciiHexDigit).ToArray();
-
- return new string(normalized);
- }
-
- private static UpdaterOverrides LoadUpdaterOverrides()
- {
- using RegistryKey? key = Registry.LocalMachine.OpenSubKey(REGISTRY_PATH);
-
-#if DEBUG
- if (key is not null)
- {
- Logger.Info($"Updater registry overrides loaded from HKLM\\{REGISTRY_PATH}");
- }
-
- return new UpdaterOverrides(
- GetRegistryString(key, REG_PRODUCTINFO_URL) ?? DEFAULT_PRODUCTINFO_URL,
- GetRegistryString(key, REG_PRODUCTINFO_KEY) ?? DEFAULT_PRODUCTINFO_KEY,
- GetRegistryBool(key, REG_ALLOW_UNSAFE_URLS),
- GetRegistryBool(key, REG_SKIP_HASH_VALIDATION),
- GetRegistryBool(key, REG_SKIP_SIGNER_THUMBPRINT_CHECK),
- GetRegistryBool(key, REG_DISABLE_TLS_VALIDATION)
- );
-#else
- LogIgnoredReleaseOverrides(key);
- string productInfoUrl =
- GetRegistryString(key, REG_PRODUCTINFO_URL) ?? DEFAULT_PRODUCTINFO_URL;
-
- return new UpdaterOverrides(
- productInfoUrl,
- DEFAULT_PRODUCTINFO_KEY,
- false,
- false,
- false,
- false
- );
-#endif
- }
-
-#if !DEBUG
- private static void LogIgnoredReleaseOverrides(RegistryKey? key)
- {
- if (key is null)
- {
- return;
- }
-
- foreach (string valueName in RELEASE_IGNORED_REGISTRY_VALUES)
- {
- if (key.GetValue(valueName) is not null)
- {
- Logger.Warn(
- $"Release build is ignoring updater registry value HKLM\\{REGISTRY_PATH}\\{valueName}."
- );
- }
- }
- }
-#endif
-
- private static string? GetRegistryString(RegistryKey? key, string valueName)
- {
-#pragma warning disable CA1416
- object? value = key?.GetValue(valueName);
-#pragma warning restore CA1416
- if (value is null)
- {
- return null;
- }
-
- string? parsedValue = value.ToString();
- if (string.IsNullOrWhiteSpace(parsedValue))
- {
- return null;
- }
-
- return parsedValue.Trim();
- }
-
-#if DEBUG
- private static bool GetRegistryBool(RegistryKey? key, string valueName)
- {
-#pragma warning disable CA1416
- object? value = key?.GetValue(valueName);
-#pragma warning restore CA1416
- if (value is null)
- {
- return false;
- }
-
- if (value is int intValue)
- {
- return intValue != 0;
- }
-
- if (value is long longValue)
- {
- return longValue != 0;
- }
-
- string normalized = value.ToString()?.Trim() ?? "";
- return normalized.Equals("1", StringComparison.OrdinalIgnoreCase)
- || normalized.Equals("true", StringComparison.OrdinalIgnoreCase)
- || normalized.Equals("yes", StringComparison.OrdinalIgnoreCase)
- || normalized.Equals("on", StringComparison.OrdinalIgnoreCase);
- }
-#endif
-
- private sealed record UpdateCandidate(
- bool IsUpgradable,
- string VersionName,
- string InstallerHash,
- string InstallerDownloadUrl,
- string SourceName
- );
-
- private sealed record UpdaterOverrides(
- string ProductInfoUrl,
- string ProductInfoProductKey,
- bool AllowUnsafeUrls,
- bool SkipHashValidation,
- bool SkipSignerThumbprintCheck,
- bool DisableTlsValidation
- );
-
- private sealed class ProductInfoProduct
- {
- public ProductInfoChannel? Current { get; set; }
- public ProductInfoChannel? Beta { get; set; }
- }
-
- private sealed class ProductInfoChannel
- {
- public string Version { get; set; } = string.Empty;
- public List Files { get; set; } = [];
- }
-
- private sealed class ProductInfoFile
- {
- public string Arch { get; set; } = string.Empty;
- public string Type { get; set; } = string.Empty;
- public string Url { get; set; } = string.Empty;
- public string Hash { get; set; } = string.Empty;
- }
-
- [JsonSourceGenerationOptions(AllowTrailingCommas = true)]
- [JsonSerializable(typeof(Dictionary))]
- private sealed partial class AutoUpdaterJsonContext : JsonSerializerContext { }
}
diff --git a/src/UniGetUI/CLIHandler.cs b/src/UniGetUI/CLIHandler.cs
index 6a404add25..e600ab177c 100644
--- a/src/UniGetUI/CLIHandler.cs
+++ b/src/UniGetUI/CLIHandler.cs
@@ -45,16 +45,20 @@ public static int Help()
public static int ImportSettings()
{
- var args = Environment.GetCommandLineArgs().ToList();
+ return ImportSettings(Environment.GetCommandLineArgs());
+ }
- var filePos = args.IndexOf(IMPORT_SETTINGS);
+ internal static int ImportSettings(IReadOnlyList