mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-01 07:59:36 +02:00
Compare commits
16 Commits
autoUpgrad
...
copilot/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab08e318d8 | ||
|
|
ce0439f580 | ||
|
|
5b7d3c0a82 | ||
|
|
5e12d2476d | ||
|
|
3b6bf9c0ce | ||
|
|
9020b18a9f | ||
|
|
f59f67cb08 | ||
|
|
890ea40f8a | ||
|
|
70dd9db67a | ||
|
|
fb7c945a2c | ||
|
|
c72580c8f2 | ||
|
|
ce6debf68b | ||
|
|
1eca1713e1 | ||
|
|
1254cba088 | ||
|
|
b5373cbb2b | ||
|
|
b82e6c508d |
7
.github/actions/spell-check/allow/code.txt
vendored
7
.github/actions/spell-check/allow/code.txt
vendored
@@ -328,6 +328,9 @@ MRUCMPPROC
|
||||
MRUINFO
|
||||
REGSTR
|
||||
|
||||
# Quick Accent
|
||||
Ene
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
INVOKEIDLIST
|
||||
MEMORYSTATUSEX
|
||||
@@ -389,5 +392,9 @@ nostdin
|
||||
engtype
|
||||
Nonpaged
|
||||
|
||||
# Spell-check fragments
|
||||
traies
|
||||
udit
|
||||
|
||||
# XAML
|
||||
Untargeted
|
||||
|
||||
5
.github/actions/spell-check/expect.txt
vendored
5
.github/actions/spell-check/expect.txt
vendored
@@ -791,7 +791,6 @@ lowlevel
|
||||
LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
lpch
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
@@ -833,7 +832,6 @@ lstrlen
|
||||
LTEXT
|
||||
LTRREADING
|
||||
luid
|
||||
LUMA
|
||||
lusrmgr
|
||||
LVal
|
||||
LWA
|
||||
@@ -852,7 +850,6 @@ MAPPEDTOSAMEKEY
|
||||
MAPTOSAMESHORTCUT
|
||||
MAPVK
|
||||
MARKDOWNPREVIEWHANDLERCPP
|
||||
MAXDWORD
|
||||
MAXSHORTCUTSIZE
|
||||
maxversiontested
|
||||
MBM
|
||||
@@ -917,7 +914,6 @@ MOUSEINPUT
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
MRM
|
||||
MRT
|
||||
mru
|
||||
msc
|
||||
mscorlib
|
||||
@@ -2303,7 +2299,6 @@ virama
|
||||
vnd
|
||||
vredraw
|
||||
VSpeed
|
||||
VSync
|
||||
WASDK
|
||||
WCRAPI
|
||||
wft
|
||||
|
||||
@@ -85,6 +85,14 @@
|
||||
<ForceImportBeforeCppProps>$(RepoRoot)Cpp.Build.props</ForceImportBeforeCppProps>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Force all C# projects to reference these packages from NuGet so their 10.x versions
|
||||
take precedence over the 9.x versions bundled in the .NET 9 runtime. Without this,
|
||||
projects that don't transitively depend on these packages would get the runtime 9.x
|
||||
version, causing deps.json version conflicts with projects that do. -->
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<PackageReference Include="System.Diagnostics.EventLog" />
|
||||
<PackageReference Include="System.Threading.Channels" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
|
||||
@@ -42,26 +42,26 @@
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.250303.1" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.4.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.4.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.AI.Foundry.Local" Version="0.3.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.66.0-beta" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.74.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.74.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.74.0-beta" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.74.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.74.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.74.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
@@ -89,11 +89,11 @@
|
||||
<PackageVersion Include="MSTest" Version="$(MSTestVersion)" />
|
||||
<PackageVersion Include="MSTest.TestFramework" Version="$(MSTestVersion)" />
|
||||
<PackageVersion Include="NJsonSchema" Version="11.4.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="NLog" Version="5.2.8" />
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||
<PackageVersion Include="OpenAI" Version="2.9.1" />
|
||||
<PackageVersion Include="Polly.Core" Version="8.6.5" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
@@ -113,23 +113,27 @@
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.10" />
|
||||
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
|
||||
<!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.10" />
|
||||
<!-- Package System.Diagnostics.EventLog force-pinned to 10.x to match other Microsoft.Extensions packages;
|
||||
referenced by all C# projects (via Directory.Build.props) to override the 9.x version from the .NET runtime. -->
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="10.0.5" />
|
||||
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.10" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.7.0" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.9.0" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.10" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.11" />
|
||||
<PackageVersion Include="System.Numerics.Tensors" Version="10.0.4" />
|
||||
<!-- Including System.Threading.Channels to force version across all projects;
|
||||
referenced by all C# projects (via Directory.Build.props) to override the 9.x version from the .NET runtime. -->
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.4" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.10" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="10.0.5" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
|
||||
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
|
||||
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
|
||||
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
|
||||
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
</Folder>
|
||||
<Folder Name="/common/interop/">
|
||||
@@ -792,6 +791,10 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/peek/peek/peek.vcxproj" Id="a1425b53-3d61-4679-8623-e64a0d3d0a48" />
|
||||
<Project Path="src/modules/peek/Peek.Common.UnitTests/Peek.Common.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerAccent/">
|
||||
<Project Path="src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj">
|
||||
@@ -804,6 +807,10 @@
|
||||
</Project>
|
||||
<Project Path="src/modules/poweraccent/PowerAccentKeyboardService/PowerAccentKeyboardService.vcxproj" Id="c97d9a5d-206c-454e-997e-009e227d7f02" />
|
||||
<Project Path="src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj" Id="34a354c5-23c7-4343-916c-c52daf4fc39d" />
|
||||
<Project Path="src/modules/poweraccent/PowerAccent.Core.UnitTests/PowerAccent.Core.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerOCR/">
|
||||
<Project Path="src/modules/PowerOCR/PowerOCR/PowerOCR.csproj">
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
#include <common/updating/updating.h>
|
||||
#include <common/updating/updateState.h>
|
||||
#include <common/updating/installer.h>
|
||||
#include <common/updating/configBackup.h>
|
||||
#include <common/updating/updateLifecycle.h>
|
||||
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/utils/HttpClient.h>
|
||||
@@ -23,8 +21,6 @@
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/timeutil.h>
|
||||
|
||||
#include <wil/resource.h>
|
||||
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
@@ -42,16 +38,15 @@ namespace fs = std::filesystem;
|
||||
|
||||
std::optional<fs::path> CopySelfToTempDir()
|
||||
{
|
||||
// D5 fix: Use unique temp path with PID to avoid collision on concurrent updates
|
||||
std::error_code error;
|
||||
auto dst_path = fs::temp_directory_path() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
|
||||
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
|
||||
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
|
||||
if (error)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return dst_path;
|
||||
return std::move(dst_path);
|
||||
}
|
||||
|
||||
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
@@ -62,9 +57,34 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
|
||||
auto state = UpdateState::read();
|
||||
|
||||
// Handle readyToInstall first — the installer is already on disk,
|
||||
// so we don't need a GitHub API call (which may fail if offline).
|
||||
if (state.state == UpdateState::readyToInstall)
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
Logger::error("Invoked with -update_now argument, but no update was available");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
|
||||
{
|
||||
if (!new_version_info)
|
||||
{
|
||||
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
}
|
||||
|
||||
return downloaded_installer;
|
||||
}
|
||||
else if (state.state == UpdateState::readyToInstall)
|
||||
{
|
||||
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
|
||||
if (fs::is_regular_file(installer))
|
||||
@@ -77,44 +97,12 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::upToDate)
|
||||
else if (state.state == UpdateState::upToDate)
|
||||
{
|
||||
isUpToDate = true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
|
||||
// Check for error BEFORE dereferencing — the old code crashed here
|
||||
// when GitHub API was unreachable (new_version_info held an error string).
|
||||
if (!new_version_info)
|
||||
{
|
||||
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
Logger::error("Invoked with -update_now argument, but no update was available");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
|
||||
{
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
}
|
||||
|
||||
return downloaded_installer;
|
||||
}
|
||||
|
||||
Logger::error("Invoked with -update_now argument, but update state was invalid");
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -128,29 +116,13 @@ bool InstallNewVersionStage1(fs::path installer)
|
||||
|
||||
if (pt_main_window != nullptr)
|
||||
{
|
||||
// Get the process that owns the tray window so we can wait for it to exit
|
||||
DWORD ptProcessId = 0;
|
||||
GetWindowThreadProcessId(pt_main_window, &ptProcessId);
|
||||
|
||||
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
|
||||
|
||||
// D4 fix: Wait for PT to actually exit before launching installer.
|
||||
// Without this, the installer may find PT files locked.
|
||||
if (ptProcessId != 0)
|
||||
{
|
||||
wil::unique_handle ptProcess{ OpenProcess(SYNCHRONIZE, FALSE, ptProcessId) };
|
||||
if (ptProcess)
|
||||
{
|
||||
WaitForSingleObject(ptProcess.get(), 10000); // 10 second timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the install directory so Stage 2 can relaunch PowerToys after install
|
||||
const std::wstring installDir = get_module_folderpath();
|
||||
|
||||
std::wstring arguments = updating::BuildStage2Arguments(
|
||||
UPDATE_NOW_LAUNCH_STAGE2, installer, fs::path(installDir));
|
||||
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
|
||||
arguments += L" \"";
|
||||
arguments += installer.c_str();
|
||||
arguments += L"\"";
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
|
||||
sei.lpFile = copy_in_temp->c_str();
|
||||
@@ -218,16 +190,9 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
|
||||
if (!args || nArgs < 2)
|
||||
{
|
||||
if (args)
|
||||
{
|
||||
LocalFree(args);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// D3 fix: ensure args is freed on all exit paths
|
||||
auto freeArgs = wil::scope_exit([&] { LocalFree(args); });
|
||||
|
||||
std::wstring_view action{ args[1] };
|
||||
|
||||
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
|
||||
@@ -236,10 +201,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
|
||||
if (action == UPDATE_NOW_LAUNCH_STAGE1)
|
||||
{
|
||||
// Backup config files before the update to protect against corruption
|
||||
Logger::info("Backing up config files before update");
|
||||
updating::BackupConfigFiles(fs::path(PTSettingsHelper::get_root_save_folder_location()));
|
||||
|
||||
bool isUpToDate = false;
|
||||
auto installerPath = ObtainInstaller(isUpToDate);
|
||||
bool failed = !installerPath.has_value();
|
||||
@@ -256,12 +217,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
}
|
||||
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
|
||||
{
|
||||
if (nArgs < 3)
|
||||
{
|
||||
Logger::error("Stage 2 invoked without installer path argument");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
const bool failed = !InstallNewVersionStage2(args[2]);
|
||||
if (failed)
|
||||
@@ -272,37 +227,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
state.state = UpdateState::errorDownloading;
|
||||
});
|
||||
}
|
||||
|
||||
// D7 fix: Always check for corrupted configs after Stage 2, regardless
|
||||
// of install success/failure. A failed install may still corrupt configs.
|
||||
Logger::info("Checking for corrupted config files after update");
|
||||
updating::RestoreCorruptedConfigs(fs::path(PTSettingsHelper::get_root_save_folder_location()));
|
||||
|
||||
if (!failed)
|
||||
{
|
||||
// Relaunch PowerToys from the install directory
|
||||
if (updating::CanRelaunchAfterUpdate(nArgs))
|
||||
{
|
||||
std::wstring ptExePath = updating::BuildPowerToysExePath(args[3]);
|
||||
|
||||
Logger::info(L"Relaunching PowerToys after update: {}", ptExePath);
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
|
||||
sei.lpFile = ptExePath.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = UPDATE_REPORT_SUCCESS;
|
||||
|
||||
if (!ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::error(L"Failed to relaunch PowerToys after update");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn("Install directory not provided to Stage 2 - cannot relaunch PowerToys");
|
||||
}
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,679 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <common/updating/configBackup.h>
|
||||
#include <common/updating/updateLifecycle.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace UpdatingUnitTests
|
||||
{
|
||||
// Helper to create a temp directory for test isolation.
|
||||
// Each instance gets a unique subdirectory to prevent test interference.
|
||||
class TempDir
|
||||
{
|
||||
public:
|
||||
TempDir()
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH + 1];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
static std::atomic<int> counter{0};
|
||||
m_path = fs::path(tempPath) / (L"PowerToysUpdateTests_" + std::to_wstring(counter++));
|
||||
|
||||
// Ensure clean state
|
||||
std::error_code ec;
|
||||
fs::remove_all(m_path, ec);
|
||||
fs::create_directories(m_path, ec);
|
||||
}
|
||||
|
||||
~TempDir()
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::remove_all(m_path, ec);
|
||||
}
|
||||
|
||||
const fs::path& path() const { return m_path; }
|
||||
|
||||
// Write a file with the given content
|
||||
void WriteFile(const fs::path& relativePath, const std::string& content)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
fs::create_directories(fullPath.parent_path());
|
||||
std::ofstream file(fullPath, std::ios::binary);
|
||||
file.write(content.data(), content.size());
|
||||
}
|
||||
|
||||
// Write a file with raw bytes (including null bytes for corruption testing)
|
||||
void WriteFileBytes(const fs::path& relativePath, const std::vector<char>& bytes)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
fs::create_directories(fullPath.parent_path());
|
||||
std::ofstream file(fullPath, std::ios::binary);
|
||||
file.write(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
// Read file content as string
|
||||
std::string ReadFile(const fs::path& relativePath)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
std::ifstream file(fullPath, std::ios::binary);
|
||||
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
bool FileExists(const fs::path& relativePath)
|
||||
{
|
||||
return fs::exists(m_path / relativePath);
|
||||
}
|
||||
|
||||
private:
|
||||
fs::path m_path;
|
||||
};
|
||||
|
||||
TEST_CLASS(IsJsonFileCorruptedTests)
|
||||
{
|
||||
public:
|
||||
// Tests IsJsonFileCorrupted: valid JSON with no null bytes returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — happy path, full file scan.
|
||||
TEST_METHOD(CleanJsonFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark","startup":true})");
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: zero-length file returns false (empty is not corrupted).
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — file.read returns 0 bytes immediately.
|
||||
TEST_METHOD(EmptyFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"empty.json", "");
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"empty.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file containing embedded null bytes returns true.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — null byte detection within buffer.
|
||||
TEST_METHOD(FileWithNullBytesIsCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::vector<char> corrupted = { '{', '"', 'a', '"', ':', '\0', '\0', '\0', '}' };
|
||||
dir.WriteFileBytes(L"corrupted.json", corrupted);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"corrupted.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file entirely filled with 0x00 bytes returns true.
|
||||
// Reproduces the exact bug from #46179 where installer zeroed out JSON files.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — first byte is null.
|
||||
TEST_METHOD(FileFilledWithNullBytesIsCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::vector<char> allNulls(1024, '\0');
|
||||
dir.WriteFileBytes(L"workspaces.json", allNulls);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"workspaces.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: path that does not exist returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — file.is_open() check.
|
||||
TEST_METHOD(NonExistentFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"missing.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file larger than the 4096-byte read chunk
|
||||
// with no null bytes returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — multi-chunk while loop.
|
||||
TEST_METHOD(LargeCleanFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::string largeContent(8192, 'x');
|
||||
dir.WriteFile(L"large.json", largeContent);
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"large.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: null byte placed after the first 4096-byte
|
||||
// chunk boundary is still detected.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — second chunk scan.
|
||||
TEST_METHOD(NullByteAtEndOfLargeFileIsDetected)
|
||||
{
|
||||
TempDir dir;
|
||||
std::string content(5000, 'x');
|
||||
content[4999] = '\0';
|
||||
std::vector<char> bytes(content.begin(), content.end());
|
||||
dir.WriteFileBytes(L"sneaky.json", bytes);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"sneaky.json"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(BackupConfigFilesTests)
|
||||
{
|
||||
public:
|
||||
// Tests BackupConfigFiles: root-level .json files are copied to ConfigBackup.
|
||||
// Covers: configBackup.h BackupConfigFiles — root directory_iterator,
|
||||
// is_regular_file && extension == ".json" branch.
|
||||
// Setup: Two root-level JSON files.
|
||||
TEST_METHOD(BackupCopiesRootJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"UpdateState.json", R"({"state":0})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\UpdateState.json"));
|
||||
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: .json files inside module subdirectories are
|
||||
// copied to ConfigBackup/<module>/.
|
||||
// Covers: configBackup.h BackupConfigFiles — is_directory branch,
|
||||
// module directory_iterator with extension filter.
|
||||
// Setup: Root JSON + two module directories with JSON files.
|
||||
TEST_METHOD(BackupCopiesModuleJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[]})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
|
||||
Assert::AreEqual(std::string(R"({"zones":[]})"),
|
||||
dir.ReadFile(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: non-.json files at root level are not copied.
|
||||
// Covers: configBackup.h BackupConfigFiles — extension filter excludes .log.
|
||||
// Setup: One JSON file + one .log file at root.
|
||||
TEST_METHOD(BackupSkipsNonJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"debug.log", "log data");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\debug.log"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: the "Updates" directory is explicitly skipped.
|
||||
// Covers: configBackup.h BackupConfigFiles — dirName == L"Updates" continue.
|
||||
// Setup: Root JSON + Updates directory containing a file.
|
||||
TEST_METHOD(BackupSkipsUpdatesDirectory)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"Updates\\installer.exe", "fake exe");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\Updates"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: running backup twice overwrites the previous
|
||||
// backup with current file content.
|
||||
// Covers: configBackup.h BackupConfigFiles — fs::remove_all(backupDir) +
|
||||
// copy_options::overwrite_existing.
|
||||
// Setup: Backup, modify original, backup again.
|
||||
TEST_METHOD(BackupOverwritesPreviousBackup)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"version":1})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Update the original
|
||||
dir.WriteFile(L"settings.json", R"({"version":2})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: non-.json files inside module subdirectories
|
||||
// (e.g., FancyZones/zones.dat) should NOT be backed up.
|
||||
// Covers: configBackup.h BackupConfigFiles — extension filter in module loop.
|
||||
TEST_METHOD(BackupSkipsNonJsonFilesInModuleDirs)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
|
||||
dir.WriteFile(L"FancyZones\\zones.dat", "binary data");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\FancyZones\\zones.dat"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: empty root directory with no files produces
|
||||
// an empty ConfigBackup dir without errors.
|
||||
// Covers: configBackup.h BackupConfigFiles — empty directory_iterator.
|
||||
TEST_METHOD(BackupEmptyRootDirSucceeds)
|
||||
{
|
||||
TempDir dir;
|
||||
// Root dir exists but has no files
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(RestoreCorruptedConfigsTests)
|
||||
{
|
||||
public:
|
||||
// Tests RestoreCorruptedConfigs: corrupted root-level JSON file is restored
|
||||
// from the good backup copy.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — root file restore branch,
|
||||
// fs::exists + IsJsonFileCorrupted + backup integrity check.
|
||||
// Setup: Good file -> backup -> corrupt original -> restore.
|
||||
TEST_METHOD(RestoreFixesCorruptedRootFile)
|
||||
{
|
||||
TempDir dir;
|
||||
const std::string goodContent = R"({"theme":"dark"})";
|
||||
dir.WriteFile(L"settings.json", goodContent);
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt the original
|
||||
std::vector<char> corrupted(goodContent.size(), '\0');
|
||||
dir.WriteFileBytes(L"settings.json", corrupted);
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
Assert::AreEqual(goodContent, dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: corrupted module-level JSON file is restored
|
||||
// from the good backup copy.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — module directory branch,
|
||||
// moduleBackupEntry restore with integrity check.
|
||||
// Setup: Module file + root file -> backup -> corrupt module file -> restore.
|
||||
TEST_METHOD(RestoreFixesCorruptedModuleFile)
|
||||
{
|
||||
TempDir dir;
|
||||
const std::string goodContent = R"({"workspaces":[]})";
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", goodContent);
|
||||
dir.WriteFile(L"settings.json", R"({})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt the module file
|
||||
std::vector<char> corrupted(goodContent.size(), '\0');
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", corrupted);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::AreEqual(goodContent, dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: clean (non-corrupted) files are NOT
|
||||
// overwritten by backup — preserves user changes made after backup.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — IsJsonFileCorrupted
|
||||
// returns false, copy_file is skipped.
|
||||
// Setup: File -> backup -> modify (but keep valid) -> restore.
|
||||
TEST_METHOD(RestoreLeavesCleanFilesUntouched)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"version":1})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Modify original (but keep it clean JSON)
|
||||
dir.WriteFile(L"settings.json", R"({"version":2})");
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Should NOT have been restored since it's not corrupted
|
||||
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when no ConfigBackup directory exists,
|
||||
// restore silently does nothing (no crash, no data loss).
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — !fs::exists(backupDir)
|
||||
// early return.
|
||||
// Setup: File with no prior backup.
|
||||
TEST_METHOD(RestoreHandlesMissingBackupDirectory)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
|
||||
// No backup was created - restore should silently do nothing
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: end-to-end scenario with multiple modules,
|
||||
// some corrupted and some clean, verifying selective restore.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — both root and module
|
||||
// branches, selective restore based on corruption status.
|
||||
// Setup: 4 modules -> backup -> corrupt 2 -> restore -> verify all 4.
|
||||
TEST_METHOD(FullBackupAndRestoreRoundTrip)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// Set up a realistic config structure
|
||||
dir.WriteFile(L"settings.json", R"({"startup":true,"theme":"dark"})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[{"id":1}]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[{"name":"dev"}]})");
|
||||
dir.WriteFile(L"KeyboardManager\\default.json", R"({"remaps":[]})");
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt some files (simulating #46179 scenario)
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(100, '\0'));
|
||||
dir.WriteFileBytes(L"settings.json", std::vector<char>(50, '\0'));
|
||||
// Leave FancyZones and KBM clean
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Corrupted files should be restored
|
||||
Assert::AreEqual(std::string(R"({"startup":true,"theme":"dark"})"), dir.ReadFile(L"settings.json"));
|
||||
Assert::AreEqual(std::string(R"({"workspaces":[{"name":"dev"}]})"), dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
|
||||
// Clean files should be unchanged
|
||||
Assert::AreEqual(std::string(R"({"zones":[{"id":1}]})"), dir.ReadFile(L"FancyZones\\settings.json"));
|
||||
Assert::AreEqual(std::string(R"({"remaps":[]})"), dir.ReadFile(L"KeyboardManager\\default.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when the original file has been deleted
|
||||
// (not corrupted), restore should NOT recreate it from backup. The installer
|
||||
// may have intentionally removed obsolete config files.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — fs::exists guard.
|
||||
TEST_METHOD(RestoreSkipsDeletedOriginals)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"obsolete.json", R"({"old":true})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Installer deletes the file
|
||||
std::error_code ec;
|
||||
fs::remove(dir.path() / L"obsolete.json", ec);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Should NOT be recreated
|
||||
Assert::IsFalse(dir.FileExists(L"obsolete.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when the backup file itself is corrupted
|
||||
// (e.g., disk error during backup), restore should NOT copy corrupted
|
||||
// backup over the original — that would make things worse.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — backup integrity check (B2 fix).
|
||||
TEST_METHOD(RestoreSkipsCorruptedBackup)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt BOTH the original AND the backup
|
||||
std::vector<char> nulls(50, '\0');
|
||||
dir.WriteFileBytes(L"settings.json", nulls);
|
||||
dir.WriteFileBytes(L"ConfigBackup\\settings.json", nulls);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Original should still be corrupted — we don't restore from bad backup
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
}
|
||||
};
|
||||
|
||||
// Simulates what actually happens during a PowerToys upgrade:
|
||||
// 1. User has settings from normal use
|
||||
// 2. Updater backs up before install (Stage 1)
|
||||
// 3. Installer runs and corrupts some files (simulated)
|
||||
// 4. Updater restores corrupted files (Stage 2)
|
||||
// 5. PT relaunches and finds working configs
|
||||
TEST_CLASS(UpgradeSimulationTests)
|
||||
{
|
||||
public:
|
||||
// Tests full upgrade simulation: backup -> installer corrupts files -> restore.
|
||||
// Verifies that corrupted files are restored and clean files are untouched.
|
||||
// Covers: configBackup.h BackupConfigFiles + RestoreCorruptedConfigs —
|
||||
// end-to-end with 5 modules, 2 corrupted, 3 clean.
|
||||
// Setup: Realistic config structure with multiple modules.
|
||||
TEST_METHOD(SimulateUpgradeWithCorruption)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// === User's real config state before upgrade ===
|
||||
dir.WriteFile(L"settings.json",
|
||||
R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json",
|
||||
R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json",
|
||||
R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})");
|
||||
dir.WriteFile(L"KeyboardManager\\default.json",
|
||||
R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})");
|
||||
dir.WriteFile(L"MouseWithoutBorders\\settings.json",
|
||||
R"({"machineKey":"abc123","connectToAll":true})");
|
||||
|
||||
// Non-JSON files that should be left alone
|
||||
dir.WriteFile(L"update.log", "2026-04-11 update started");
|
||||
|
||||
// === Stage 1: Backup before killing PT ===
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Verify backup was created correctly
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\KeyboardManager\\default.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\MouseWithoutBorders\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\update.log"));
|
||||
|
||||
// === Installer runs: some files get corrupted (the #46179 scenario) ===
|
||||
// Workspaces JSON filled with null bytes
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(512, '\0'));
|
||||
// Main settings partially corrupted (null bytes injected)
|
||||
std::vector<char> partialCorrupt = { '{', '"', 's', '\0', '\0', '\0', '\0', '}' };
|
||||
dir.WriteFileBytes(L"settings.json", partialCorrupt);
|
||||
|
||||
// FancyZones, KBM, and MWB survive the install fine
|
||||
// (this is realistic - not all files get corrupted)
|
||||
|
||||
// === Stage 2: Restore after install completes ===
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// === Verify: PT relaunches and finds working configs ===
|
||||
|
||||
// Corrupted files should be restored from backup
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"Workspaces\\workspaces.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})"),
|
||||
dir.ReadFile(L"settings.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})"),
|
||||
dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
|
||||
// Clean files should be untouched (not overwritten with backup)
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})"),
|
||||
dir.ReadFile(L"FancyZones\\settings.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})"),
|
||||
dir.ReadFile(L"KeyboardManager\\default.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"machineKey":"abc123","connectToAll":true})"),
|
||||
dir.ReadFile(L"MouseWithoutBorders\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests upgrade from an old version that has fewer modules than the new version.
|
||||
// Verifies that new module configs (created by the installer) are not touched
|
||||
// by restore, while corrupted old configs are restored.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — module dir in root that
|
||||
// has no corresponding backup entry.
|
||||
// Setup: Old version with 1 module -> backup -> new installer adds module -> corrupt old -> restore.
|
||||
TEST_METHOD(SimulateUpgradeFromVeryOldVersion)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// Old version had fewer modules - only settings.json
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark","powertoys_version":"v0.60.0"})");
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// New installer creates new module dirs that didn't exist before
|
||||
dir.WriteFile(L"NewModule\\settings.json", R"({"enabled":true})");
|
||||
|
||||
// Old settings get corrupted during upgrade
|
||||
dir.WriteFileBytes(L"settings.json", std::vector<char>(100, '\0'));
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Old settings restored
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"theme":"dark","powertoys_version":"v0.60.0"})"),
|
||||
dir.ReadFile(L"settings.json"));
|
||||
|
||||
// New module settings untouched (no backup existed for them)
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"enabled":true})"),
|
||||
dir.ReadFile(L"NewModule\\settings.json"));
|
||||
}
|
||||
};
|
||||
|
||||
// Tests for the update lifecycle: argument passing between Stage 1 and Stage 2,
|
||||
// relaunch path construction, and the handoff that was broken in #42004/#43011/#44071.
|
||||
TEST_CLASS(UpdateLifecycleTests)
|
||||
{
|
||||
public:
|
||||
// Tests BuildStage2Arguments: output contains the stage 2 flag, installer path,
|
||||
// and install directory — all three components needed for Stage 2.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — concatenation logic.
|
||||
// Setup: Typical paths with spaces (Program Files).
|
||||
TEST_METHOD(BuildStage2ArgumentsContainsInstallerAndInstallDir)
|
||||
{
|
||||
const auto args = updating::BuildStage2Arguments(
|
||||
L"-update_now_stage_2",
|
||||
L"C:\\Users\\test\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-x64.exe",
|
||||
L"C:\\Program Files\\PowerToys");
|
||||
|
||||
// Must contain the stage 2 flag
|
||||
Assert::IsTrue(args.find(L"-update_now_stage_2") != std::wstring::npos);
|
||||
// Must contain the installer path (quoted)
|
||||
Assert::IsTrue(args.find(L"powertoyssetup-x64.exe") != std::wstring::npos);
|
||||
// Must contain the install directory (quoted) — this was MISSING before our fix
|
||||
Assert::IsTrue(args.find(L"C:\\Program Files\\PowerToys") != std::wstring::npos);
|
||||
}
|
||||
|
||||
// Tests BuildStage2Arguments: both paths are wrapped in double quotes to
|
||||
// survive CommandLineToArgvW parsing when paths contain spaces.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — quote wrapping.
|
||||
// Setup: Installer path with spaces.
|
||||
TEST_METHOD(BuildStage2ArgumentsQuotesBothPaths)
|
||||
{
|
||||
const auto args = updating::BuildStage2Arguments(
|
||||
L"-update_now_stage_2",
|
||||
L"C:\\path with spaces\\installer.exe",
|
||||
L"C:\\Program Files\\PowerToys");
|
||||
|
||||
// Count quotes — should have 4 (open/close for each path)
|
||||
size_t quoteCount = std::count(args.begin(), args.end(), L'"');
|
||||
Assert::AreEqual(size_t{ 4 }, quoteCount);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: appends "PowerToys.exe" to the install dir.
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path / operator.
|
||||
// Setup: Standard install path without trailing backslash.
|
||||
TEST_METHOD(BuildPowerToysExePathAppendsExeName)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys");
|
||||
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: trailing backslash does not produce double
|
||||
// backslash (e.g., "...PowerToys\\PowerToys.exe").
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path normalizes separators.
|
||||
// Setup: Install path with trailing backslash.
|
||||
TEST_METHOD(BuildPowerToysExePathHandlesTrailingBackslash)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys\\");
|
||||
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: empty string produces just "PowerToys.exe".
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path with empty input.
|
||||
// Setup: Empty install directory string.
|
||||
TEST_METHOD(BuildPowerToysExePathHandlesEmptyString)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"");
|
||||
Assert::AreEqual(std::wstring(L"PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests CanRelaunchAfterUpdate: returns true when Stage 2 receives
|
||||
// the install directory (argCount >= 4), false otherwise.
|
||||
// This is the gate that prevents relaunch when using an old Stage 1
|
||||
// that didn't pass the install dir (#42004/#43011/#44071).
|
||||
// Covers: updateLifecycle.h CanRelaunchAfterUpdate.
|
||||
TEST_METHOD(CanRelaunchReflectsArgCount)
|
||||
{
|
||||
// Old Stage 1 (pre-fix): only passed action + installer = 3 args
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(0));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(1));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(2));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(3));
|
||||
|
||||
// New Stage 1 (post-fix): passes action + installer + installDir = 4 args
|
||||
Assert::IsTrue(updating::CanRelaunchAfterUpdate(4));
|
||||
Assert::IsTrue(updating::CanRelaunchAfterUpdate(5));
|
||||
}
|
||||
|
||||
// Tests BuildStage2Arguments + CommandLineToArgvW round-trip: the exact
|
||||
// scenario where Stage 1 builds args and Windows parses them in Stage 2.
|
||||
// Verifies quoting is correct so paths with spaces survive the round trip.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — quote correctness.
|
||||
// Setup: Realistic paths with spaces and version numbers.
|
||||
TEST_METHOD(Stage2ArgumentsCanBeRoundTrippedThroughCommandLineToArgvW)
|
||||
{
|
||||
const std::wstring installerPath = L"C:\\Users\\test user\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-0.86.0-x64.exe";
|
||||
const std::wstring installDir = L"C:\\Program Files\\PowerToys";
|
||||
|
||||
const auto args = updating::BuildStage2Arguments(L"-update_now_stage_2", installerPath, installDir);
|
||||
|
||||
// Simulate what Windows does: prepend a fake exe name and parse
|
||||
std::wstring commandLine = L"PowerToys.Update.exe " + args;
|
||||
|
||||
int argc = 0;
|
||||
LPWSTR* argv = CommandLineToArgvW(commandLine.c_str(), &argc);
|
||||
Assert::IsNotNull(argv);
|
||||
Assert::AreEqual(4, argc);
|
||||
Assert::AreEqual(std::wstring(L"-update_now_stage_2"), std::wstring(argv[1]));
|
||||
Assert::AreEqual(installerPath, std::wstring(argv[2]));
|
||||
Assert::AreEqual(installDir, std::wstring(argv[3]));
|
||||
|
||||
LocalFree(argv);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>UpdatingUnitTests</RootNamespace>
|
||||
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
|
||||
<ProjectName>Updating.UnitTests</ProjectName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UpdatingUnitTests\</OutDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="UpdatingTests.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#ifndef PCH_H
|
||||
#define PCH_H
|
||||
|
||||
#include <atomic>
|
||||
#include <Windows.h>
|
||||
|
||||
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26466)
|
||||
#include "CppUnitTest.h"
|
||||
#pragma warning(pop)
|
||||
|
||||
#endif //PCH_H
|
||||
@@ -1,170 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Check if a JSON file is corrupted (contains null bytes, as seen in #46179)
|
||||
inline bool IsJsonFileCorrupted(const fs::path& filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(filePath, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t c_readChunkSize{ 4096 };
|
||||
char buffer[c_readChunkSize];
|
||||
while (file.read(buffer, c_readChunkSize) || file.gcount() > 0)
|
||||
{
|
||||
const auto bytesRead = file.gcount();
|
||||
for (std::streamsize i = 0; i < bytesRead; ++i)
|
||||
{
|
||||
if (buffer[i] == '\0')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Backup all JSON config files before update to protect against corruption (#46179)
|
||||
inline void BackupConfigFiles(const fs::path& rootPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
const fs::path backupDir = rootPath / L"ConfigBackup";
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(backupDir, ec);
|
||||
// Note: remove_all failure means stale backup may persist; continue anyway
|
||||
// since create_directories will overlay
|
||||
fs::create_directories(backupDir, ec);
|
||||
if (ec)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(rootPath, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.is_regular_file() && entry.path().extension() == L".json")
|
||||
{
|
||||
fs::copy_file(entry.path(), backupDir / entry.path().filename(), fs::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
else if (entry.is_directory())
|
||||
{
|
||||
const auto dirName = entry.path().filename().wstring();
|
||||
if (dirName == L"ConfigBackup" || dirName == L"Updates")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto moduleBackup = backupDir / entry.path().filename();
|
||||
fs::create_directories(moduleBackup, ec);
|
||||
|
||||
std::error_code moduleEc;
|
||||
for (const auto& moduleEntry : fs::directory_iterator(entry.path(), moduleEc))
|
||||
{
|
||||
if (moduleEc)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (moduleEntry.is_regular_file() && moduleEntry.path().extension() == L".json")
|
||||
{
|
||||
fs::copy_file(moduleEntry.path(), moduleBackup / moduleEntry.path().filename(), fs::copy_options::overwrite_existing, moduleEc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Intentionally swallowed — update must not fail due to backup errors.
|
||||
// Logging would require spdlog dependency which is unavailable in test context.
|
||||
}
|
||||
}
|
||||
|
||||
// Restore JSON configs from backup if corruption is detected after update
|
||||
inline void RestoreCorruptedConfigs(const fs::path& rootPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
const fs::path backupDir = rootPath / L"ConfigBackup";
|
||||
|
||||
if (!fs::exists(backupDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
for (const auto& backupEntry : fs::directory_iterator(backupDir, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (backupEntry.is_regular_file() && backupEntry.path().extension() == L".json")
|
||||
{
|
||||
const auto originalPath = rootPath / backupEntry.path().filename();
|
||||
// Only restore if the backup itself is valid
|
||||
if (fs::exists(originalPath) && IsJsonFileCorrupted(originalPath) && !IsJsonFileCorrupted(backupEntry.path()))
|
||||
{
|
||||
fs::copy_file(backupEntry.path(), originalPath, fs::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
}
|
||||
else if (backupEntry.is_directory())
|
||||
{
|
||||
const auto moduleDir = rootPath / backupEntry.path().filename();
|
||||
|
||||
std::error_code moduleEc;
|
||||
for (const auto& moduleBackupEntry : fs::directory_iterator(backupEntry.path(), moduleEc))
|
||||
{
|
||||
if (moduleEc)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (moduleBackupEntry.is_regular_file() && moduleBackupEntry.path().extension() == L".json")
|
||||
{
|
||||
const auto originalModulePath = moduleDir / moduleBackupEntry.path().filename();
|
||||
// Only restore if the backup itself is valid
|
||||
if (fs::exists(originalModulePath) && IsJsonFileCorrupted(originalModulePath) && !IsJsonFileCorrupted(moduleBackupEntry.path()))
|
||||
{
|
||||
fs::copy_file(moduleBackupEntry.path(), originalModulePath, fs::copy_options::overwrite_existing, moduleEc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Intentionally swallowed — update must not fail due to backup errors.
|
||||
// Logging would require spdlog dependency which is unavailable in test context.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Build the command-line arguments for Stage 2.
|
||||
// Stage 1 passes the installer path and the PT install directory
|
||||
// so Stage 2 can run the installer and relaunch PowerToys afterward.
|
||||
// Note: paths containing embedded double-quote characters are not supported.
|
||||
// This is safe because install paths come from get_module_folderpath().
|
||||
inline std::wstring BuildStage2Arguments(
|
||||
const std::wstring& stage2Flag,
|
||||
const fs::path& installerPath,
|
||||
const fs::path& installDir)
|
||||
{
|
||||
std::wstring arguments{ stage2Flag };
|
||||
arguments += L" \"";
|
||||
arguments += installerPath.c_str();
|
||||
arguments += L"\" \"";
|
||||
arguments += installDir.c_str();
|
||||
arguments += L"\"";
|
||||
return arguments;
|
||||
}
|
||||
|
||||
// Build the full path to PowerToys.exe from the install directory.
|
||||
// Used by Stage 2 to relaunch PT after a successful update.
|
||||
inline std::wstring BuildPowerToysExePath(const std::wstring& installDir)
|
||||
{
|
||||
return (std::filesystem::path(installDir) / L"PowerToys.exe").wstring();
|
||||
}
|
||||
|
||||
// Determine whether Stage 2 has enough information to relaunch PT.
|
||||
// Returns true if the install directory argument was provided.
|
||||
inline bool CanRelaunchAfterUpdate(int argCount)
|
||||
{
|
||||
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
|
||||
return argCount >= 4;
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,7 @@
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" />
|
||||
<PackageReference Include="System.Diagnostics.EventLog">
|
||||
<!-- This package is a dependency of Microsoft.Extensions.Logging.EventLog, but we need to set it here so we can exclude the assets, so it doesn't conflict with the 8.0.1 dll coming from .NET SDK. -->
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
|
||||
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Diagnostics.EventLog">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
|
||||
@@ -42,9 +42,7 @@
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Diagnostics.EventLog">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" />
|
||||
<PackageReference Include="System.Diagnostics.EventLog">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -35,9 +35,7 @@
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Diagnostics.EventLog">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter">
|
||||
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
|
||||
</PackageReference>
|
||||
|
||||
283
src/modules/peek/Peek.Common.UnitTests/MathHelperTests.cs
Normal file
283
src/modules/peek/Peek.Common.UnitTests/MathHelperTests.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Peek.Common.Helpers;
|
||||
|
||||
namespace Peek.Common.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class MathHelperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int)
|
||||
/// What: Verifies standard modulo for positive numbers (7 mod 3 = 1)
|
||||
/// Why: Baseline correctness — ensures the positive path matches C# % operator
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_PositiveNumbers_ShouldReturnStandardModulo()
|
||||
{
|
||||
Assert.AreEqual(1, MathHelper.Modulo(7, 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int)
|
||||
/// What: Verifies that 0 mod n = 0
|
||||
/// Why: Zero dividend is a common boundary — must not throw or return non-zero
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_ZeroDividend_ShouldReturnZero()
|
||||
{
|
||||
Assert.AreEqual(0, MathHelper.Modulo(0, 5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int)
|
||||
/// What: Verifies that exact division yields zero remainder
|
||||
/// Why: Guards against off-by-one in the modulo formula
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_ExactDivision_ShouldReturnZero()
|
||||
{
|
||||
Assert.AreEqual(0, MathHelper.Modulo(6, 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int) — negative dividend path
|
||||
/// What: Verifies that -1 mod 3 = 2 (mathematical modulo, not C# remainder)
|
||||
/// Why: C# % returns -1 for negative dividends; Modulo wraps to positive range
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_NegativeDividend_ShouldReturnPositiveResult()
|
||||
{
|
||||
Assert.AreEqual(2, MathHelper.Modulo(-1, 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int) — negative dividend with larger magnitude
|
||||
/// What: Verifies -7 mod 3 = 2 (wraps correctly for larger negative values)
|
||||
/// Why: Tests the full formula: ((-7 % 3) + 3) % 3 = (-1 + 3) % 3 = 2
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_NegativeDividend_LargerMagnitude_ShouldWrapCorrectly()
|
||||
{
|
||||
Assert.AreEqual(2, MathHelper.Modulo(-7, 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int)
|
||||
/// What: Verifies -6 mod 3 = 0 (exact negative multiple)
|
||||
/// Why: Edge case where negative dividend is an exact multiple of divisor
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_NegativeDividend_ExactMultiple_ShouldReturnZero()
|
||||
{
|
||||
Assert.AreEqual(0, MathHelper.Modulo(-6, 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int)
|
||||
/// What: Verifies n mod 1 = 0 for positive n
|
||||
/// Why: Divisor of 1 always yields 0 — guards against divide-by-zero edge case
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_PositiveDividend_DivisorOne_ShouldReturnZero()
|
||||
{
|
||||
Assert.AreEqual(0, MathHelper.Modulo(5, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int)
|
||||
/// What: Verifies -n mod 1 = 0 for negative n
|
||||
/// Why: Tests the negative path with divisor=1 (simplest negative case)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_NegativeDividend_DivisorOne_ShouldReturnZero()
|
||||
{
|
||||
Assert.AreEqual(0, MathHelper.Modulo(-5, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int)
|
||||
/// What: Verifies modulo works with large numbers (1000001 mod 1000000 = 1)
|
||||
/// Why: Tests that no integer overflow occurs in the formula for large operands
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_LargePositiveNumbers_ShouldWork()
|
||||
{
|
||||
Assert.AreEqual(1, MathHelper.Modulo(1000001, 1000000));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int)
|
||||
/// What: Verifies that dividend less than divisor returns the dividend itself
|
||||
/// Why: 2 mod 5 = 2 — no division happens, just returns the dividend
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_DividendLessThanDivisor_ShouldReturnDividend()
|
||||
{
|
||||
Assert.AreEqual(2, MathHelper.Modulo(2, 5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int) — negative dividend
|
||||
/// What: Verifies -2 mod 3 = 1 (mathematical modulo)
|
||||
/// Why: Tests another negative wrap case: ((-2 % 3) + 3) % 3 = (-2 + 3) % 3 = 1
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Modulo_NegativeDividend_MinusTwo_ModThree_ShouldReturnOne()
|
||||
{
|
||||
Assert.AreEqual(1, MathHelper.Modulo(-2, 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.Modulo(int, int) — zero divisor
|
||||
/// What: Documents that dividing by zero throws DivideByZeroException
|
||||
/// Why: Edge case — callers must handle zero divisor; the method does not guard against it
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void Modulo_ZeroDivisor_ShouldThrow()
|
||||
{
|
||||
MathHelper.Modulo(5, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void Modulo_NegativeDivisor_ShouldThrow()
|
||||
{
|
||||
MathHelper.Modulo(-1, -3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int)
|
||||
/// What: Verifies that 0 has 1 digit
|
||||
/// Why: Zero is a special case — Math.Abs(0).ToString() = "0" which has length 1
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_Zero_ShouldReturnOne()
|
||||
{
|
||||
Assert.AreEqual(1, MathHelper.NumberOfDigits(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int)
|
||||
/// What: Verifies single-digit number returns 1
|
||||
/// Why: Baseline case for the simplest positive input
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_SingleDigit_ShouldReturnOne()
|
||||
{
|
||||
Assert.AreEqual(1, MathHelper.NumberOfDigits(5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int)
|
||||
/// What: Verifies two-digit number returns 2
|
||||
/// Why: Tests the transition from single to multi-digit
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_TwoDigits_ShouldReturnTwo()
|
||||
{
|
||||
Assert.AreEqual(2, MathHelper.NumberOfDigits(42));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int)
|
||||
/// What: Verifies three-digit number (100) returns 3
|
||||
/// Why: Tests exact power of 10 boundary (100 vs 99)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_ThreeDigits_ShouldReturnThree()
|
||||
{
|
||||
Assert.AreEqual(3, MathHelper.NumberOfDigits(100));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int) — negative number
|
||||
/// What: Verifies that sign is ignored via Math.Abs
|
||||
/// Why: Negative sign is not a digit — only magnitude matters
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_NegativeNumber_ShouldIgnoreSign()
|
||||
{
|
||||
Assert.AreEqual(3, MathHelper.NumberOfDigits(-123));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int) — int.MaxValue
|
||||
/// What: Verifies int.MaxValue (2147483647) returns 10 digits
|
||||
/// Why: Upper boundary of int range — ensures no overflow in Math.Abs path
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_MaxValue_ShouldReturnTenDigits()
|
||||
{
|
||||
Assert.AreEqual(10, MathHelper.NumberOfDigits(int.MaxValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int) — near int.MinValue
|
||||
/// What: Verifies int.MinValue + 1 (-2147483647) returns 10 digits
|
||||
/// Why: int.MinValue itself overflows Math.Abs — this tests the safe boundary
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_MinValue_ShouldHandleAbsoluteValue()
|
||||
{
|
||||
Assert.AreEqual(10, MathHelper.NumberOfDigits(int.MinValue + 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int) — int.MinValue
|
||||
/// What: Documents that int.MinValue causes OverflowException due to Math.Abs
|
||||
/// Why: Edge case — Math.Abs(-2147483648) has no positive int representation
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(OverflowException))]
|
||||
public void NumberOfDigits_MinValue_ShouldThrowOverflow()
|
||||
{
|
||||
MathHelper.NumberOfDigits(int.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int) — powers of 10
|
||||
/// What: Verifies digit count at each power-of-10 boundary (1, 10, 100, 1000, 10000)
|
||||
/// Why: Powers of 10 are the exact transition points — off-by-one bugs surface here
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_PowerOfTen_ShouldReturnCorrectCount()
|
||||
{
|
||||
Assert.AreEqual(1, MathHelper.NumberOfDigits(1));
|
||||
Assert.AreEqual(2, MathHelper.NumberOfDigits(10));
|
||||
Assert.AreEqual(3, MathHelper.NumberOfDigits(100));
|
||||
Assert.AreEqual(4, MathHelper.NumberOfDigits(1000));
|
||||
Assert.AreEqual(5, MathHelper.NumberOfDigits(10000));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int) — 9→10 boundary
|
||||
/// What: Verifies that 9 returns 1 digit and 10 returns 2 digits
|
||||
/// Why: Tests the first single-to-double digit transition
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_BoundaryValues_NineAndTen()
|
||||
{
|
||||
Assert.AreEqual(1, MathHelper.NumberOfDigits(9));
|
||||
Assert.AreEqual(2, MathHelper.NumberOfDigits(10));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: MathHelper.NumberOfDigits(int) — 99→100 boundary
|
||||
/// What: Verifies that 99 returns 2 digits and 100 returns 3 digits
|
||||
/// Why: Tests the double-to-triple digit transition
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NumberOfDigits_BoundaryValues_NinetyNineAndHundred()
|
||||
{
|
||||
Assert.AreEqual(2, MathHelper.NumberOfDigits(99));
|
||||
Assert.AreEqual(3, MathHelper.NumberOfDigits(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
156
src/modules/peek/Peek.Common.UnitTests/PathHelperTests.cs
Normal file
156
src/modules/peek/Peek.Common.UnitTests/PathHelperTests.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Peek.Common.Helpers;
|
||||
|
||||
namespace Peek.Common.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class PathHelperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a standard UNC path (\\server\share) is recognized
|
||||
/// Why: Baseline positive case — the most common UNC format
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_StandardUncPath_ShouldReturnTrue()
|
||||
{
|
||||
Assert.IsTrue(PathHelper.IsUncPath(@"\\server\share"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a UNC path with nested subfolders and file is recognized
|
||||
/// Why: Real-world UNC paths include subfolders — must not fail on deeper paths
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_UncPathWithSubfolder_ShouldReturnTrue()
|
||||
{
|
||||
Assert.IsTrue(PathHelper.IsUncPath(@"\\server\share\folder\file.txt"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a UNC path with dotted server name (FQDN) is recognized
|
||||
/// Why: Enterprise environments use FQDN server names (e.g., server.corp.com)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_UncPathWithDottedServer_ShouldReturnTrue()
|
||||
{
|
||||
Assert.IsTrue(PathHelper.IsUncPath(@"\\server.domain.com\share"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a UNC path with IP address is recognized
|
||||
/// Why: Some network shares use IP addresses instead of hostnames
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_UncPathWithIPAddress_ShouldReturnTrue()
|
||||
{
|
||||
Assert.IsTrue(PathHelper.IsUncPath(@"\\192.168.1.1\share"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a local drive path (C:\...) is not classified as UNC
|
||||
/// Why: Drive-letter paths are local — confusing them with UNC would break file access
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_LocalDrivePath_ShouldReturnFalse()
|
||||
{
|
||||
Assert.IsFalse(PathHelper.IsUncPath(@"C:\Users\test\file.txt"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a bare drive root (D:\) is not classified as UNC
|
||||
/// Why: Drive roots are local paths — shortest possible local absolute path
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_LocalRootPath_ShouldReturnFalse()
|
||||
{
|
||||
Assert.IsFalse(PathHelper.IsUncPath(@"D:\"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a relative path is not classified as UNC
|
||||
/// Why: Relative paths have no server component — Uri.TryCreate fails for them
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_RelativePath_ShouldReturnFalse()
|
||||
{
|
||||
Assert.IsFalse(PathHelper.IsUncPath(@"folder\file.txt"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies an empty string returns false without throwing
|
||||
/// Why: Empty input is a common edge case — must not crash
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_EmptyString_ShouldReturnFalse()
|
||||
{
|
||||
Assert.IsFalse(PathHelper.IsUncPath(string.Empty));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies an HTTP URL is not classified as UNC
|
||||
/// Why: HTTP URLs are not file paths — Uri.IsUnc must return false for them
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_HttpUrl_ShouldReturnFalse()
|
||||
{
|
||||
Assert.IsFalse(PathHelper.IsUncPath("http://example.com/path"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a file:/// URI (local file) is not classified as UNC
|
||||
/// Why: file:///C:/... is a local file URI, not a network UNC path
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_FileUri_ShouldReturnFalse()
|
||||
{
|
||||
Assert.IsFalse(PathHelper.IsUncPath("file:///C:/Users/test/file.txt"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a standard UNC path with a file component (\\server\share\file.txt)
|
||||
/// Why: Tests UNC with a trailing file name — distinct from share-only paths
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_StandardUncWithFile_ShouldReturnTrue()
|
||||
{
|
||||
Assert.IsTrue(PathHelper.IsUncPath(@"\\server\share\file.txt"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies a single-backslash prefix is not classified as UNC
|
||||
/// Why: UNC requires exactly two leading backslashes — one backslash is not UNC
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_SingleBackslash_ShouldReturnFalse()
|
||||
{
|
||||
Assert.IsFalse(PathHelper.IsUncPath(@"\server\share"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: PathHelper.IsUncPath(string)
|
||||
/// What: Verifies null input returns false without throwing
|
||||
/// Why: Null is a common edge case — Uri.TryCreate handles null gracefully
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IsUncPath_NullInput_ShouldReturnFalse()
|
||||
{
|
||||
Assert.IsFalse(PathHelper.IsUncPath(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\Peek.Common.Tests\</OutputPath>
|
||||
<RootNamespace>Peek.Common.UnitTests</RootNamespace>
|
||||
<AssemblyName>PowerToys.Peek.Common.UnitTests</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<!-- Pull in the WindowsDesktop shared framework so that runtime DLLs (System.CodeDom,
|
||||
Microsoft.VisualBasic, WindowsBase, etc.) resolve to the same version as all other
|
||||
projects in the solution. Without this, transitive NuGet packages provide older
|
||||
versions that fail the verifyDepsJsonLibraryVersions CI check. -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Peek.Common\Peek.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -11,6 +11,11 @@ namespace Peek.Common.Helpers
|
||||
{
|
||||
public static int Modulo(int a, int b)
|
||||
{
|
||||
if (b <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(b), b, "Divisor must be positive.");
|
||||
}
|
||||
|
||||
return a < 0 ? ((a % b) + b) % b : a % b;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerAccent.Core.Services;
|
||||
using PowerAccent.Core.Tools;
|
||||
|
||||
namespace PowerAccent.Core.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class CalculationTests
|
||||
{
|
||||
// Screen representing a standard 1920x1080 monitor at position (0,0)
|
||||
private static readonly Rect StandardScreen = new Rect(0, 0, 1920, 1080);
|
||||
|
||||
// A typical toolbar window size (300x50 in WPF DIPs)
|
||||
private static readonly Size ToolbarWindow = new Size(300, 50);
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromCaret
|
||||
/// What: Verifies toolbar is centered horizontally above the caret at screen center
|
||||
/// Why: The most common placement scenario — regression here affects every user
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_CenterOfScreen_ShouldCenterToolbar()
|
||||
{
|
||||
var caret = new Point(960, 540);
|
||||
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
|
||||
|
||||
// X should be caret.X - window.Width/2 = 960 - 150 = 810
|
||||
Assert.AreEqual(810.0, result.X);
|
||||
|
||||
// Y should be caret.Y - window.Height - 20 = 540 - 50 - 20 = 470
|
||||
Assert.AreEqual(470.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromCaret — left edge clamping
|
||||
/// What: Verifies toolbar is clamped to screen left when caret is near left edge
|
||||
/// Why: Without clamping, toolbar would extend off-screen and be unusable
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_NearLeftEdge_ShouldClampToScreenLeft()
|
||||
{
|
||||
var caret = new Point(50, 540);
|
||||
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
|
||||
|
||||
// left = 50 - 150 = -100, which is < screen.X (0), so X should be clamped to 0
|
||||
Assert.AreEqual(0.0, result.X);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromCaret — right edge clamping
|
||||
/// What: Verifies toolbar is clamped to screen right when caret is near right edge
|
||||
/// Why: Toolbar extending past screen right would be clipped by the display
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_NearRightEdge_ShouldClampToScreenRight()
|
||||
{
|
||||
var caret = new Point(1900, 540);
|
||||
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
|
||||
|
||||
// left = 1900 - 150 = 1750, left + window.Width = 1750 + 300 = 2050 > 1920
|
||||
// So X should be clamped to screen.X + screen.Width - window.Width = 1920 - 300 = 1620
|
||||
Assert.AreEqual(1620.0, result.X);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromCaret — top edge flip
|
||||
/// What: Verifies toolbar is placed below the caret when there is no room above
|
||||
/// Why: When caret is near top of screen, toolbar must flip below to remain visible
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_NearTopEdge_ShouldPlaceBelow()
|
||||
{
|
||||
var caret = new Point(960, 30);
|
||||
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
|
||||
|
||||
// top = 30 - 50 - 20 = -40, which is < screen.Y (0)
|
||||
// So Y should be caret.Y + 20 = 50 (placed below caret)
|
||||
Assert.AreEqual(50.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromCaret — normal above placement
|
||||
/// What: Verifies toolbar is placed above the caret when there is sufficient space
|
||||
/// Why: Default behavior — toolbar should appear above to avoid occluding text below
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_SufficientSpaceAbove_ShouldPlaceAbove()
|
||||
{
|
||||
var caret = new Point(960, 200);
|
||||
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
|
||||
|
||||
// top = 200 - 50 - 20 = 130, which is >= screen.Y (0)
|
||||
Assert.AreEqual(130.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromCaret — multi-monitor offset
|
||||
/// What: Verifies toolbar respects non-zero screen origin (second monitor at X=1920)
|
||||
/// Why: Multi-monitor setups have offset screen coordinates — clamping must use screen.X
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_OffsetScreen_ShouldRespectScreenOrigin()
|
||||
{
|
||||
var screen = new Rect(1920, 0, 1920, 1080);
|
||||
var caret = new Point(1950, 540);
|
||||
var result = Calculation.GetRawCoordinatesFromCaret(caret, screen, ToolbarWindow);
|
||||
|
||||
// left = 1950 - 150 = 1800, which is < screen.X (1920)
|
||||
// So X should be clamped to 1920
|
||||
Assert.AreEqual(1920.0, result.X);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromCaret — exact boundary case
|
||||
/// What: Verifies no clamping occurs when toolbar fits exactly at the boundary
|
||||
/// Why: Off-by-one errors in boundary checks are common — this catches ≤ vs < bugs
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromCaret_ExactBoundary_ShouldNotClamp()
|
||||
{
|
||||
var caret = new Point(150, 100);
|
||||
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
|
||||
|
||||
// left = 150 - 150 = 0, which == screen.X, so no left clamp needed
|
||||
Assert.AreEqual(0.0, result.X);
|
||||
|
||||
// top = 100 - 50 - 20 = 30, which >= 0
|
||||
Assert.AreEqual(30.0, result.Y);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NotImplementedException))]
|
||||
public void GetRawCoordinatesFromPosition_InvalidPosition_ShouldThrow()
|
||||
{
|
||||
Calculation.GetRawCoordinatesFromPosition((Position)999, StandardScreen, ToolbarWindow, 1.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Top
|
||||
/// What: Verifies toolbar is horizontally centered at the top of the screen with offset
|
||||
/// Why: Exact value test — catches formula regressions in the Top positioning branch
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_TopCenter_ShouldBeCenteredAtTop()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.Top, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + screen.Width/2 - window.Width*dpi/2 = 0 + 960 - 150 = 810
|
||||
Assert.AreEqual(810.0, result.X);
|
||||
|
||||
// Y: screen.Y + offset = 0 + 24 = 24
|
||||
Assert.AreEqual(24.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Bottom
|
||||
/// What: Verifies toolbar is horizontally centered at the bottom of the screen
|
||||
/// Why: Bottom placement subtracts window height + offset — easy to get wrong
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_BottomCenter_ShouldBeCenteredAtBottom()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.Bottom, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: centered = 810
|
||||
Assert.AreEqual(810.0, result.X);
|
||||
|
||||
// Y: screen.Y + screen.Height - (window.Height*dpi + offset) = 0 + 1080 - (50 + 24) = 1006
|
||||
Assert.AreEqual(1006.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Center
|
||||
/// What: Verifies toolbar is centered both horizontally and vertically
|
||||
/// Why: Center is the simplest case — validates the baseline formula for both axes
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_Center_ShouldBeTrulyCentered()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.Center, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + screen.Width/2 - window.Width*dpi/2 = 0 + 960 - 150 = 810
|
||||
Assert.AreEqual(810.0, result.X);
|
||||
|
||||
// Y: screen.Y + screen.Height/2 - window.Height*dpi/2 = 0 + 540 - 25 = 515
|
||||
Assert.AreEqual(515.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.TopLeft
|
||||
/// What: Verifies toolbar is placed at top-left corner with offset margin
|
||||
/// Why: Corner placements use raw offset — validates the simplest X/Y path
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_TopLeft_ShouldBeAtTopLeftCorner()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.TopLeft, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + offset = 0 + 24 = 24
|
||||
Assert.AreEqual(24.0, result.X);
|
||||
|
||||
// Y: screen.Y + offset = 0 + 24 = 24
|
||||
Assert.AreEqual(24.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.TopRight
|
||||
/// What: Verifies exact coordinates for top-right corner placement
|
||||
/// Why: TopRight combines right-aligned X with top Y — validates both formula branches
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_TopRight_ShouldBeAtTopRightCorner()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.TopRight, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + screen.Width - (window.Width*dpi + offset) = 0 + 1920 - (300 + 24) = 1596
|
||||
Assert.AreEqual(1596.0, result.X);
|
||||
|
||||
// Y: screen.Y + offset = 0 + 24 = 24
|
||||
Assert.AreEqual(24.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.BottomLeft
|
||||
/// What: Verifies exact coordinates for bottom-left corner placement
|
||||
/// Why: BottomLeft combines left-aligned X with bottom Y — validates both formula branches
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_BottomLeft_ShouldBeAtBottomLeftCorner()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.BottomLeft, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + offset = 0 + 24 = 24
|
||||
Assert.AreEqual(24.0, result.X);
|
||||
|
||||
// Y: screen.Y + screen.Height - (window.Height*dpi + offset) = 0 + 1080 - (50 + 24) = 1006
|
||||
Assert.AreEqual(1006.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.BottomRight
|
||||
/// What: Verifies toolbar is placed at bottom-right corner with offset margin
|
||||
/// Why: BottomRight uses the most complex formula (subtracts from both edges)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_BottomRight_ShouldBeAtBottomRightCorner()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.BottomRight, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + screen.Width - (window.Width*dpi + offset) = 0 + 1920 - (300 + 24) = 1596
|
||||
Assert.AreEqual(1596.0, result.X);
|
||||
|
||||
// Y: screen.Y + screen.Height - (window.Height*dpi + offset) = 0 + 1080 - (50 + 24) = 1006
|
||||
Assert.AreEqual(1006.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Left
|
||||
/// What: Verifies toolbar is placed at left edge, vertically centered
|
||||
/// Why: Left position uses offset for X and centering for Y — tests the mix
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_Left_ShouldBeAtLeftMiddle()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.Left, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + offset = 0 + 24 = 24
|
||||
Assert.AreEqual(24.0, result.X);
|
||||
|
||||
// Y: centered vertically = 0 + 540 - 25 = 515
|
||||
Assert.AreEqual(515.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Right
|
||||
/// What: Verifies toolbar is placed at right edge, vertically centered
|
||||
/// Why: Right position subtracts window width from screen edge — tests the right-align formula
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_Right_ShouldBeAtRightMiddle()
|
||||
{
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.Right, StandardScreen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + screen.Width - (window.Width*dpi + offset) = 1920 - 324 = 1596
|
||||
Assert.AreEqual(1596.0, result.X);
|
||||
|
||||
// Y: centered vertically = 515
|
||||
Assert.AreEqual(515.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with DPI=1.5
|
||||
/// What: Verifies that DPI scaling is applied to window size in centering formula
|
||||
/// Why: High-DPI monitors are common — incorrect scaling makes toolbar appear off-center
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_WithHighDpi_ShouldScaleWindow()
|
||||
{
|
||||
double dpi = 1.5;
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.Center, StandardScreen, ToolbarWindow, dpi);
|
||||
|
||||
// X: screen.X + screen.Width/2 - window.Width*dpi/2 = 0 + 960 - (300*1.5)/2 = 960 - 225 = 735
|
||||
Assert.AreEqual(735.0, result.X);
|
||||
|
||||
// Y: screen.Y + screen.Height/2 - window.Height*dpi/2 = 0 + 540 - (50*1.5)/2 = 540 - 37.5 = 502.5
|
||||
Assert.AreEqual(502.5, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with DPI=2.0, TopLeft
|
||||
/// What: Verifies that offset is NOT DPI-scaled for corner positions
|
||||
/// Why: The 24px offset is a fixed margin — DPI scaling only affects window size
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_WithDpi2_TopLeft_ShouldUseOffset()
|
||||
{
|
||||
double dpi = 2.0;
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.TopLeft, StandardScreen, ToolbarWindow, dpi);
|
||||
|
||||
// X: screen.X + offset = 0 + 24 = 24 (offset is not DPI-scaled)
|
||||
Assert.AreEqual(24.0, result.X);
|
||||
|
||||
// Y: screen.Y + offset = 0 + 24 = 24
|
||||
Assert.AreEqual(24.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with DPI=2.0, BottomRight
|
||||
/// What: Verifies DPI scaling of window size in bottom-right corner formula
|
||||
/// Why: At 2x DPI, the window occupies 600×100 pixels — offset from edge must account for this
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_WithDpi2_BottomRight_ShouldScaleWindowSize()
|
||||
{
|
||||
double dpi = 2.0;
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.BottomRight, StandardScreen, ToolbarWindow, dpi);
|
||||
|
||||
// X: screen.X + screen.Width - (window.Width*dpi + offset) = 0 + 1920 - (300*2 + 24) = 1920 - 624 = 1296
|
||||
Assert.AreEqual(1296.0, result.X);
|
||||
|
||||
// Y: screen.Y + screen.Height - (window.Height*dpi + offset) = 0 + 1080 - (50*2 + 24) = 1080 - 124 = 956
|
||||
Assert.AreEqual(956.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Calculation.GetRawCoordinatesFromPosition with offset screen origin
|
||||
/// What: Verifies screen.X is added as the base for TopLeft on a secondary monitor
|
||||
/// Why: Without adding screen origin, toolbar appears on the wrong monitor
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetRawCoordinatesFromPosition_OffsetScreen_ShouldRespectScreenOrigin()
|
||||
{
|
||||
var screen = new Rect(1920, 0, 1920, 1080);
|
||||
var result = Calculation.GetRawCoordinatesFromPosition(Position.TopLeft, screen, ToolbarWindow, 1.0);
|
||||
|
||||
// X: screen.X + offset = 1920 + 24 = 1944
|
||||
Assert.AreEqual(1944.0, result.X);
|
||||
|
||||
// Y: screen.Y + offset = 0 + 24 = 24
|
||||
Assert.AreEqual(24.0, result.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerToys.PowerAccentKeyboardService;
|
||||
|
||||
namespace PowerAccent.Core.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class LanguagesTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey
|
||||
/// What: Verifies that passing an empty language array returns an empty result
|
||||
/// Why: Guards against NullReferenceException or incorrect fallback when no languages are configured
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_EmptyLanguageArray_ShouldReturnEmpty()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_A, Array.Empty<Language>());
|
||||
Assert.AreEqual(0, result.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.FR
|
||||
/// What: Verifies that French returns at least one accented character for the letter A
|
||||
/// Why: Regression guard — ensures the French language mapping is wired up and non-empty
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_SingleLanguage_ShouldReturnNonEmpty()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.FR });
|
||||
Assert.IsTrue(result.Length > 0, "French should have accented characters for A");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with multiple languages
|
||||
/// What: Verifies that combining French and Spanish merges characters and removes duplicates
|
||||
/// Why: Guards against duplicate entries when languages share accent characters (e.g., à)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_MultipleLanguages_ShouldMergeAndDeduplicate()
|
||||
{
|
||||
var frResult = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.FR });
|
||||
var spResult = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.SP });
|
||||
var combinedResult = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.FR, Language.SP });
|
||||
|
||||
Assert.AreEqual(combinedResult.Length, combinedResult.Distinct().Count(), "Combined result should contain no duplicates");
|
||||
|
||||
foreach (var ch in frResult)
|
||||
{
|
||||
CollectionAssert.Contains(combinedResult, ch, $"Combined result should contain French character '{ch}'");
|
||||
}
|
||||
|
||||
foreach (var ch in spResult)
|
||||
{
|
||||
CollectionAssert.Contains(combinedResult, ch, $"Combined result should contain Spanish character '{ch}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.FR for VK_A
|
||||
/// What: Verifies that French returns specific expected accent characters (à, â, æ)
|
||||
/// Why: Pinpoints exact character expectations — catches silent data regressions in language tables
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_French_A_ShouldContainExpectedAccents()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.FR });
|
||||
|
||||
CollectionAssert.Contains(result, "à", "French should contain à");
|
||||
CollectionAssert.Contains(result, "â", "French should contain â");
|
||||
CollectionAssert.Contains(result, "æ", "French should contain æ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.DE for VK_O
|
||||
/// What: Verifies German returns the ö umlaut for the O key
|
||||
/// Why: Core German character — regression would break German users' workflow
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_German_O_ShouldContainUmlaut()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_O, new[] { Language.DE });
|
||||
CollectionAssert.Contains(result, "ö", "German should contain ö for O");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.DE for VK_U
|
||||
/// What: Verifies German returns the ü umlaut for the U key
|
||||
/// Why: Core German character — regression would break German users' workflow
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_German_U_ShouldContainUmlaut()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_U, new[] { Language.DE });
|
||||
CollectionAssert.Contains(result, "ü", "German should contain ü for U");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.DE for VK_S
|
||||
/// What: Verifies German returns the ß (Eszett) for the S key
|
||||
/// Why: ß is unique to German — its absence would be a critical language regression
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_German_S_ShouldContainEszett()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_S, new[] { Language.DE });
|
||||
CollectionAssert.Contains(result, "ß", "German should contain ß for S");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.SP for VK_N
|
||||
/// What: Verifies Spanish returns the ñ character for the N key
|
||||
/// Why: ñ is the most recognizable Spanish-specific character
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_Spanish_N_ShouldContainEne()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_N, new[] { Language.SP });
|
||||
CollectionAssert.Contains(result, "ñ", "Spanish should contain ñ for N");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.CZ for VK_C
|
||||
/// What: Verifies Czech returns the č (c-caron) for the C key
|
||||
/// Why: č is essential for Czech — its absence would break Czech language support
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_Czech_C_ShouldContainCaron()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_C, new[] { Language.CZ });
|
||||
CollectionAssert.Contains(result, "č", "Czech should contain č for C");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.PL for VK_L
|
||||
/// What: Verifies Polish returns the ł (L with stroke) for the L key
|
||||
/// Why: ł is fundamental to Polish — its absence would break Polish language support
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_Polish_L_ShouldContainStroke()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_L, new[] { Language.PL });
|
||||
CollectionAssert.Contains(result, "ł", "Polish should contain ł for L");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.VI for VK_A
|
||||
/// What: Verifies Vietnamese returns a rich set of A variants (≥10 accented forms)
|
||||
/// Why: Vietnamese has extensive diacritics — guards against truncation of the character table
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_Vietnamese_A_ShouldContainMultipleAccents()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.VI });
|
||||
|
||||
Assert.IsTrue(result.Length >= 10, "Vietnamese should have many accented A characters");
|
||||
CollectionAssert.Contains(result, "à", "Vietnamese should contain à");
|
||||
CollectionAssert.Contains(result, "ă", "Vietnamese should contain ă");
|
||||
CollectionAssert.Contains(result, "â", "Vietnamese should contain â");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.SPECIAL for VK_0
|
||||
/// What: Verifies that digit 0 returns superscript (⁰) and subscript (₀) variants
|
||||
/// Why: Special characters for digits are used in mathematical/scientific input
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_Special_Digits_ShouldReturnSubscriptsSuperscripts()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_0, new[] { Language.SPECIAL });
|
||||
Assert.IsTrue(result.Length > 0, "Special characters for 0 should exist");
|
||||
CollectionAssert.Contains(result, "⁰", "Special 0 should contain superscript 0");
|
||||
CollectionAssert.Contains(result, "₀", "Special 0 should contain subscript 0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.SPECIAL for VK_1
|
||||
/// What: Verifies that digit 1 returns fraction (½) and superscript (¹) variants
|
||||
/// Why: Fraction characters are commonly needed for recipes, measurements, etc.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_Special_1_ShouldContainFractions()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_1, new[] { Language.SPECIAL });
|
||||
CollectionAssert.Contains(result, "½", "Special 1 should contain ½");
|
||||
CollectionAssert.Contains(result, "¹", "Special 1 should contain ¹");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.CUR
|
||||
/// What: Verifies that Currency language returns well-known currency symbols (€, $, £)
|
||||
/// Why: Currency symbols are the primary use case — tests exact symbol presence, not just non-null
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_Currency_ShouldReturnCurrencySymbols()
|
||||
{
|
||||
var euroResult = Languages.GetDefaultLetterKey(LetterKey.VK_E, new[] { Language.CUR });
|
||||
CollectionAssert.Contains(euroResult, "€", "Currency VK_E should contain Euro symbol");
|
||||
|
||||
var dollarResult = Languages.GetDefaultLetterKey(LetterKey.VK_S, new[] { Language.CUR });
|
||||
CollectionAssert.Contains(dollarResult, "$", "Currency VK_S should contain Dollar symbol");
|
||||
|
||||
var poundResult = Languages.GetDefaultLetterKey(LetterKey.VK_P, new[] { Language.CUR });
|
||||
CollectionAssert.Contains(poundResult, "£", "Currency VK_P should contain Pound symbol");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey with Language.DE for VK_Q
|
||||
/// What: Verifies that a key with no accented characters for a language returns empty
|
||||
/// Why: Guards against spurious characters appearing for keys that should have no accents
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_UnusedKeyForLanguage_ShouldReturnEmpty()
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(LetterKey.VK_Q, new[] { Language.DE });
|
||||
Assert.AreEqual(0, result.Length, "German should have no accented characters for Q");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKeyALL (cache path via ConcurrentDictionary)
|
||||
/// What: Verifies that calling with all languages twice returns the same cached array instance
|
||||
/// Why: Cache hit should return the identical object reference, not a recomputed copy
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_AllLanguages_ShouldReturnCachedResults()
|
||||
{
|
||||
var allLanguages = Enum.GetValues<Language>();
|
||||
|
||||
var result1 = Languages.GetDefaultLetterKey(LetterKey.VK_A, allLanguages);
|
||||
var result2 = Languages.GetDefaultLetterKey(LetterKey.VK_A, allLanguages);
|
||||
|
||||
Assert.IsTrue(result1.Length > 0, "All languages combined should have accented A characters");
|
||||
CollectionAssert.AreEqual(result1, result2, "Cached results should be identical");
|
||||
|
||||
// Verify cache hit returns same object instance, not just equal contents
|
||||
Assert.AreSame(result1, result2, "Second call should return cached reference");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Languages.GetDefaultLetterKey across all LetterKey × Language combinations
|
||||
/// What: Smoke-tests every letter key with all languages to ensure no exceptions are thrown
|
||||
/// Why: Catches missing switch arms or null returns that would crash the toolbar at runtime
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetDefaultLetterKey_AllLanguages_EachLetterKey_ShouldNotThrow()
|
||||
{
|
||||
var allLanguages = Enum.GetValues<Language>();
|
||||
var allLetterKeys = Enum.GetValues<LetterKey>();
|
||||
|
||||
foreach (var letterKey in allLetterKeys)
|
||||
{
|
||||
var result = Languages.GetDefaultLetterKey(letterKey, allLanguages);
|
||||
Assert.IsNotNull(result, $"Result for {letterKey} should not be null");
|
||||
|
||||
Assert.AreEqual(result.Length, result.Distinct().Count(), $"Result for {letterKey} should have no duplicates");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
205
src/modules/poweraccent/PowerAccent.Core.UnitTests/PointTests.cs
Normal file
205
src/modules/poweraccent/PowerAccent.Core.UnitTests/PointTests.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace PowerAccent.Core.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class PointTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Product code: Point() default constructor
|
||||
/// What: Verifies X and Y initialize to zero
|
||||
/// Why: Default state must be deterministic — uninitialized coordinates cause misplacement
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DefaultConstructor_ShouldInitializeToZero()
|
||||
{
|
||||
var point = new Point();
|
||||
Assert.AreEqual(0, point.X);
|
||||
Assert.AreEqual(0, point.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point(double, double) constructor
|
||||
/// What: Verifies that fractional coordinates are preserved exactly
|
||||
/// Why: Sub-pixel precision is needed for DPI-aware positioning
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DoubleConstructor_ShouldSetCoordinates()
|
||||
{
|
||||
var point = new Point(3.5, 7.2);
|
||||
Assert.AreEqual(3.5, point.X);
|
||||
Assert.AreEqual(7.2, point.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point(int, int) constructor
|
||||
/// What: Verifies integer coordinates are stored as doubles without loss
|
||||
/// Why: Implicit int→double conversion must be exact for pixel-aligned positioning
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IntConstructor_ShouldSetCoordinates()
|
||||
{
|
||||
var point = new Point(10, 20);
|
||||
Assert.AreEqual(10.0, point.X);
|
||||
Assert.AreEqual(20.0, point.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point(System.Drawing.Point) constructor
|
||||
/// What: Verifies conversion from System.Drawing.Point preserves coordinates
|
||||
/// Why: Win32 API interop uses System.Drawing.Point — conversion must be lossless
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DrawingPointConstructor_ShouldConvertCoordinates()
|
||||
{
|
||||
var drawingPoint = new System.Drawing.Point(15, 25);
|
||||
var point = new Point(drawingPoint);
|
||||
Assert.AreEqual(15.0, point.X);
|
||||
Assert.AreEqual(25.0, point.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point implicit operator from System.Drawing.Point
|
||||
/// What: Verifies implicit conversion works at assignment sites
|
||||
/// Why: Enables seamless interop without explicit casts in calling code
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void ImplicitConversion_FromDrawingPoint_ShouldWork()
|
||||
{
|
||||
Point point = new System.Drawing.Point(100, 200);
|
||||
Assert.AreEqual(100.0, point.X);
|
||||
Assert.AreEqual(200.0, point.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point operator /(Point, double)
|
||||
/// What: Verifies scalar division halves both coordinates
|
||||
/// Why: Core arithmetic for DPI normalization
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_ShouldDivideCoordinates()
|
||||
{
|
||||
var point = new Point(10.0, 20.0);
|
||||
var result = point / 2.0;
|
||||
Assert.AreEqual(5.0, result.X);
|
||||
Assert.AreEqual(10.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point operator /(Point, double) with negative divisor
|
||||
/// What: Verifies negative divisor negates both coordinates
|
||||
/// Why: Ensures correct sign propagation in coordinate math
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_WithNegativeDivider_ShouldNegateCoordinates()
|
||||
{
|
||||
var point = new Point(10.0, 20.0);
|
||||
var result = point / -2.0;
|
||||
Assert.AreEqual(-5.0, result.X);
|
||||
Assert.AreEqual(-10.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point operator /(Point, double) zero guard
|
||||
/// What: Verifies that dividing by zero throws DivideByZeroException
|
||||
/// Why: Prevents Infinity coordinates from corrupting toolbar placement
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByScalar_WithZero_ShouldThrow()
|
||||
{
|
||||
var point = new Point(10.0, 20.0);
|
||||
_ = point / 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point operator /(Point, Point)
|
||||
/// What: Verifies component-wise division (X/X, Y/Y)
|
||||
/// Why: Used for coordinate space transformations
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByPoint_ShouldDivideComponentwise()
|
||||
{
|
||||
var point = new Point(10.0, 20.0);
|
||||
var divider = new Point(2.0, 5.0);
|
||||
var result = point / divider;
|
||||
Assert.AreEqual(5.0, result.X);
|
||||
Assert.AreEqual(4.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point operator /(Point, Point) zero guard for X
|
||||
/// What: Verifies that a divider with X=0 throws DivideByZeroException
|
||||
/// Why: X=0 in a divider would produce Infinity for the X coordinate
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByPoint_WithZeroX_ShouldThrow()
|
||||
{
|
||||
var point = new Point(10.0, 20.0);
|
||||
var divider = new Point(0.0, 5.0);
|
||||
_ = point / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point operator /(Point, Point) zero guard for Y
|
||||
/// What: Verifies that a divider with Y=0 throws DivideByZeroException
|
||||
/// Why: Y=0 in a divider would produce Infinity for the Y coordinate
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByPoint_WithZeroY_ShouldThrow()
|
||||
{
|
||||
var point = new Point(10.0, 20.0);
|
||||
var divider = new Point(5.0, 0.0);
|
||||
_ = point / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point(double, double) constructor with negatives
|
||||
/// What: Verifies that negative coordinates are stored correctly
|
||||
/// Why: Negative coordinates are valid in multi-monitor setups (left/above primary)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NegativeCoordinates_ShouldBeAllowed()
|
||||
{
|
||||
var point = new Point(-5.5, -10.3);
|
||||
Assert.AreEqual(-5.5, point.X);
|
||||
Assert.AreEqual(-10.3, point.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point operator /(Point, double) with fractional divisor
|
||||
/// What: Verifies dividing by 0.5 effectively doubles coordinates
|
||||
/// Why: Fractional DPI values (1.25x, 1.5x) are common in production
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_WithFractionalDivider_ShouldWork()
|
||||
{
|
||||
var point = new Point(10.0, 20.0);
|
||||
var result = point / 0.5;
|
||||
Assert.AreEqual(20.0, result.X);
|
||||
Assert.AreEqual(40.0, result.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Point operator /(Point, Point) zero guard for both X and Y
|
||||
/// What: Verifies that a divider with both X=0 and Y=0 throws
|
||||
/// Why: Degenerate origin-point divider must be rejected — tests the first guard hit
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByPoint_WithBothZero_ShouldThrow()
|
||||
{
|
||||
var point = new Point(10.0, 20.0);
|
||||
var divider = new Point(0.0, 0.0);
|
||||
_ = point / divider;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\PowerAccent.Core.Tests\</OutputPath>
|
||||
<RootNamespace>PowerAccent.Core.UnitTests</RootNamespace>
|
||||
<AssemblyName>PowerToys.PowerAccent.Core.UnitTests</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PowerAccent.Core\PowerAccent.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
223
src/modules/poweraccent/PowerAccent.Core.UnitTests/RectTests.cs
Normal file
223
src/modules/poweraccent/PowerAccent.Core.UnitTests/RectTests.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace PowerAccent.Core.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class RectTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Product code: Rect() default constructor
|
||||
/// What: Verifies all properties initialize to zero
|
||||
/// Why: Default state must be well-defined to avoid uninitialized geometry bugs
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DefaultConstructor_ShouldInitializeToZero()
|
||||
{
|
||||
var rect = new Rect();
|
||||
Assert.AreEqual(0, rect.X);
|
||||
Assert.AreEqual(0, rect.Y);
|
||||
Assert.AreEqual(0, rect.Width);
|
||||
Assert.AreEqual(0, rect.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect(int, int, int, int) constructor
|
||||
/// What: Verifies that integer arguments are correctly stored as doubles
|
||||
/// Why: Implicit int→double conversion must preserve exact values
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IntConstructor_ShouldSetAllProperties()
|
||||
{
|
||||
var rect = new Rect(10, 20, 800, 600);
|
||||
Assert.AreEqual(10.0, rect.X);
|
||||
Assert.AreEqual(20.0, rect.Y);
|
||||
Assert.AreEqual(800.0, rect.Width);
|
||||
Assert.AreEqual(600.0, rect.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect(double, double, double, double) constructor
|
||||
/// What: Verifies that fractional double values are stored exactly
|
||||
/// Why: Ensures no rounding or truncation occurs during construction
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DoubleConstructor_ShouldSetAllProperties()
|
||||
{
|
||||
var rect = new Rect(1.5, 2.5, 100.3, 200.7);
|
||||
Assert.AreEqual(1.5, rect.X);
|
||||
Assert.AreEqual(2.5, rect.Y);
|
||||
Assert.AreEqual(100.3, rect.Width);
|
||||
Assert.AreEqual(200.7, rect.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect(Point, Size) constructor
|
||||
/// What: Verifies that Point and Size components map to correct Rect properties
|
||||
/// Why: X/Y come from Point, Width/Height from Size — a swap would silently corrupt layout
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void PointSizeConstructor_ShouldSetFromComponents()
|
||||
{
|
||||
var point = new Point(50.0, 100.0);
|
||||
var size = new Size(400.0, 300.0);
|
||||
var rect = new Rect(point, size);
|
||||
Assert.AreEqual(50.0, rect.X);
|
||||
Assert.AreEqual(100.0, rect.Y);
|
||||
Assert.AreEqual(400.0, rect.Width);
|
||||
Assert.AreEqual(300.0, rect.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, double)
|
||||
/// What: Verifies component-wise division by a scalar
|
||||
/// Why: Used for DPI scaling — incorrect division would misposition the toolbar
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_ShouldDivideAllComponents()
|
||||
{
|
||||
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
|
||||
var result = rect / 2.0;
|
||||
Assert.AreEqual(5.0, result.X);
|
||||
Assert.AreEqual(10.0, result.Y);
|
||||
Assert.AreEqual(50.0, result.Width);
|
||||
Assert.AreEqual(100.0, result.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, double) zero guard
|
||||
/// What: Verifies that dividing by zero throws DivideByZeroException
|
||||
/// Why: Prevents Infinity values from propagating through layout calculations
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByScalar_WithZero_ShouldThrow()
|
||||
{
|
||||
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
|
||||
_ = rect / 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, Rect)
|
||||
/// What: Verifies component-wise division (X/X, Y/Y, Width/Width, Height/Height)
|
||||
/// Why: Used for coordinate system transformations between screen and window space
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByRect_ShouldDivideComponentwise()
|
||||
{
|
||||
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
|
||||
var divider = new Rect(2.0, 5.0, 10.0, 20.0);
|
||||
var result = rect / divider;
|
||||
Assert.AreEqual(5.0, result.X);
|
||||
Assert.AreEqual(4.0, result.Y);
|
||||
Assert.AreEqual(10.0, result.Width);
|
||||
Assert.AreEqual(10.0, result.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, Rect) zero guard for X component
|
||||
/// What: Verifies that a divider with X=0 throws DivideByZeroException
|
||||
/// Why: X=0 in a divider Rect would produce Infinity for the X coordinate
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByRect_WithZeroX_ShouldThrow()
|
||||
{
|
||||
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
|
||||
var divider = new Rect(0.0, 5.0, 10.0, 20.0);
|
||||
_ = rect / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, Rect) zero guard for Y component
|
||||
/// What: Verifies that a divider with Y=0 throws DivideByZeroException
|
||||
/// Why: Y=0 in a divider Rect would produce Infinity for the Y coordinate
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByRect_WithZeroY_ShouldThrow()
|
||||
{
|
||||
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
|
||||
var divider = new Rect(5.0, 0.0, 10.0, 20.0);
|
||||
_ = rect / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, Rect) zero guard for Width component
|
||||
/// What: Verifies that a divider with Width=0 throws DivideByZeroException
|
||||
/// Why: Guards against producing Infinity values that propagate through calculations
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByRect_WithZeroWidth_ShouldThrow()
|
||||
{
|
||||
var rect = new Rect(10, 20, 100, 200);
|
||||
var divider = new Rect(1, 1, 0, 1);
|
||||
_ = rect / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, Rect) zero guard for Height component
|
||||
/// What: Verifies that a divider with Height=0 throws DivideByZeroException
|
||||
/// Why: Guards against producing Infinity values that propagate through calculations
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByRect_WithZeroHeight_ShouldThrow()
|
||||
{
|
||||
var rect = new Rect(10, 20, 100, 200);
|
||||
var divider = new Rect(1, 1, 1, 0);
|
||||
_ = rect / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, Rect) zero guard for all components
|
||||
/// What: Verifies that a divider with all zero components throws DivideByZeroException
|
||||
/// Why: Degenerate all-zero divider must be caught — tests the first guard hit (X=0)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByRect_WithAllZeroComponents_ShouldThrow()
|
||||
{
|
||||
var rect = new Rect(10, 20, 100, 200);
|
||||
var divider = new Rect(0, 0, 0, 0);
|
||||
_ = rect / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect(double, double, double, double) constructor
|
||||
/// What: Verifies that negative X/Y coordinates are allowed
|
||||
/// Why: Multi-monitor setups can have negative screen coordinates (left/above primary)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void NegativeCoordinates_ShouldBeAllowed()
|
||||
{
|
||||
var rect = new Rect(-100.0, -200.0, 400.0, 300.0);
|
||||
Assert.AreEqual(-100.0, rect.X);
|
||||
Assert.AreEqual(-200.0, rect.Y);
|
||||
Assert.AreEqual(400.0, rect.Width);
|
||||
Assert.AreEqual(300.0, rect.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Rect operator /(Rect, double) with negative divisor
|
||||
/// What: Verifies that negative divisor negates all components
|
||||
/// Why: Ensures sign propagation is correct — used in coordinate flipping scenarios
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_WithNegativeDivider_ShouldNegateComponents()
|
||||
{
|
||||
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
|
||||
var result = rect / -1.0;
|
||||
Assert.AreEqual(-10.0, result.X);
|
||||
Assert.AreEqual(-20.0, result.Y);
|
||||
Assert.AreEqual(-100.0, result.Width);
|
||||
Assert.AreEqual(-200.0, result.Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
192
src/modules/poweraccent/PowerAccent.Core.UnitTests/SizeTests.cs
Normal file
192
src/modules/poweraccent/PowerAccent.Core.UnitTests/SizeTests.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace PowerAccent.Core.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class SizeTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Product code: Size() default constructor
|
||||
/// What: Verifies Width and Height initialize to zero
|
||||
/// Why: Default state must be deterministic to avoid layout corruption
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DefaultConstructor_ShouldInitializeToZero()
|
||||
{
|
||||
var size = new Size();
|
||||
Assert.AreEqual(0, size.Width);
|
||||
Assert.AreEqual(0, size.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size(double, double) constructor
|
||||
/// What: Verifies fractional values are preserved exactly
|
||||
/// Why: Ensures no rounding during construction for sub-pixel precision
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DoubleConstructor_ShouldSetDimensions()
|
||||
{
|
||||
var size = new Size(100.5, 200.3);
|
||||
Assert.AreEqual(100.5, size.Width);
|
||||
Assert.AreEqual(200.3, size.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size(int, int) constructor
|
||||
/// What: Verifies integer dimensions are stored as doubles without loss
|
||||
/// Why: Implicit int→double conversion must be exact
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void IntConstructor_ShouldSetDimensions()
|
||||
{
|
||||
var size = new Size(800, 600);
|
||||
Assert.AreEqual(800.0, size.Width);
|
||||
Assert.AreEqual(600.0, size.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size implicit operator from System.Drawing.Size
|
||||
/// What: Verifies implicit conversion from System.Drawing.Size works
|
||||
/// Why: Interop boundary — System.Drawing types are used by Win32 APIs
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void ImplicitConversion_FromDrawingSize_ShouldWork()
|
||||
{
|
||||
Size size = new System.Drawing.Size(1920, 1080);
|
||||
Assert.AreEqual(1920.0, size.Width);
|
||||
Assert.AreEqual(1080.0, size.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, double)
|
||||
/// What: Verifies scalar division halves both dimensions
|
||||
/// Why: Core arithmetic used for DPI scaling — must produce exact results
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_ShouldDivideDimensions()
|
||||
{
|
||||
var size = new Size(100.0, 200.0);
|
||||
var result = size / 2.0;
|
||||
Assert.AreEqual(50.0, result.Width);
|
||||
Assert.AreEqual(100.0, result.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, double) with fractional divisor
|
||||
/// What: Verifies that dividing by 0.5 effectively doubles dimensions
|
||||
/// Why: Fractional DPI values are real (e.g., 1.25x, 1.5x scaling)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_WithFractionalDivider_ShouldWork()
|
||||
{
|
||||
var size = new Size(100.0, 200.0);
|
||||
var result = size / 0.5;
|
||||
Assert.AreEqual(200.0, result.Width);
|
||||
Assert.AreEqual(400.0, result.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, double) zero guard
|
||||
/// What: Verifies that dividing by zero throws DivideByZeroException
|
||||
/// Why: Prevents Infinity dimensions that would corrupt window layout
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionByScalar_WithZero_ShouldThrow()
|
||||
{
|
||||
var size = new Size(100.0, 200.0);
|
||||
_ = size / 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, Size)
|
||||
/// What: Verifies component-wise division (Width/Width, Height/Height)
|
||||
/// Why: Used for computing scaling ratios between two rectangles
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionBySize_ShouldDivideComponentwise()
|
||||
{
|
||||
var size = new Size(100.0, 200.0);
|
||||
var divider = new Size(10.0, 20.0);
|
||||
var result = size / divider;
|
||||
Assert.AreEqual(10.0, result.Width);
|
||||
Assert.AreEqual(10.0, result.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, Size) zero Width guard
|
||||
/// What: Verifies that a divider with Width=0 throws DivideByZeroException
|
||||
/// Why: Width=0 divider would produce Infinity — must be caught early
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionBySize_WithZeroWidth_ShouldThrow()
|
||||
{
|
||||
var size = new Size(100.0, 200.0);
|
||||
var divider = new Size(0.0, 20.0);
|
||||
_ = size / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, Size) zero Height guard
|
||||
/// What: Verifies that a divider with Height=0 throws DivideByZeroException
|
||||
/// Why: Height=0 divider would produce Infinity — must be caught early
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionBySize_WithZeroHeight_ShouldThrow()
|
||||
{
|
||||
var size = new Size(100.0, 200.0);
|
||||
var divider = new Size(10.0, 0.0);
|
||||
_ = size / divider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, double) with negative divisor
|
||||
/// What: Verifies that negative divisor correctly negates both dimensions
|
||||
/// Why: Ensures sign propagation is correct in arithmetic
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_WithNegativeDivider_ShouldWork()
|
||||
{
|
||||
var size = new Size(100.0, 200.0);
|
||||
var result = size / -2.0;
|
||||
Assert.AreEqual(-50.0, result.Width);
|
||||
Assert.AreEqual(-100.0, result.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, double) with very small divisor
|
||||
/// What: Verifies division by a small number produces expected large result
|
||||
/// Why: Near-zero divisors (e.g., very low DPI) must not crash or produce NaN
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DivisionByScalar_WithVerySmallDivider_ShouldYieldLargeResult()
|
||||
{
|
||||
var size = new Size(1.0, 1.0);
|
||||
var result = size / 0.001;
|
||||
Assert.AreEqual(1000.0, result.Width, 0.01);
|
||||
Assert.AreEqual(1000.0, result.Height, 0.01);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Product code: Size operator /(Size, Size) zero guard for both dimensions
|
||||
/// What: Verifies that a divider with both Width=0 and Height=0 throws
|
||||
/// Why: Degenerate zero-size divider must be rejected — tests the first guard hit
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(DivideByZeroException))]
|
||||
public void DivisionBySize_WithBothZeroDimensions_ShouldThrow()
|
||||
{
|
||||
var size = new Size(100.0, 200.0);
|
||||
var divider = new Size(0.0, 0.0);
|
||||
_ = size / divider;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public struct Rect
|
||||
|
||||
public static Rect operator /(Rect rect, Rect divider)
|
||||
{
|
||||
if (divider.X == 0 || divider.Y == 0)
|
||||
if (divider.X == 0 || divider.Y == 0 || divider.Width == 0 || divider.Height == 0)
|
||||
{
|
||||
throw new DivideByZeroException();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public struct Size
|
||||
|
||||
public static Size operator /(Size size, Size divider)
|
||||
{
|
||||
if (divider.Width == 0 || divider.Height == 0 || divider.Width == 0 || divider.Height == 0)
|
||||
if (divider.Width == 0 || divider.Height == 0)
|
||||
{
|
||||
throw new DivideByZeroException();
|
||||
}
|
||||
|
||||
@@ -33,4 +33,10 @@
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>PowerToys.PowerAccent.Core.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -28,11 +28,7 @@
|
||||
so it doesn't conflict with the dll coming from .NET SDK. -->
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Diagnostics.EventLog">
|
||||
<!-- This package is a transitive dependency, but we need to set it here so we can exclude the assets,
|
||||
so it doesn't conflict with the dll coming from .NET SDK. -->
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -11,7 +11,10 @@ namespace cmdArg
|
||||
// restarting it from there, so it doesn't interfere with the installation process.
|
||||
const inline wchar_t* UPDATE_NOW_LAUNCH_STAGE1 = L"-update_now";
|
||||
// Stage 2 consists of starting the installer and optionally launching newly installed PowerToys binary.
|
||||
// That's indicated by the following 2 flags.
|
||||
const inline wchar_t* UPDATE_NOW_LAUNCH_STAGE2 = L"-update_now_stage_2";
|
||||
const inline wchar_t* UPDATE_STAGE2_RESTART_PT = L"restart";
|
||||
const inline wchar_t* UPDATE_STAGE2_DONT_START_PT = L"dont_start";
|
||||
|
||||
const inline wchar_t* UPDATE_REPORT_SUCCESS = L"-report_update_success";
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
QuickAccessShortcut = new HotkeySettings();
|
||||
IsElevated = false;
|
||||
ShowNewUpdatesToastNotification = true;
|
||||
AutoDownloadUpdates = true;
|
||||
AutoDownloadUpdates = false;
|
||||
EnableExperimentation = true;
|
||||
DashboardSortOrder = DashboardSortOrder.Alphabetical;
|
||||
Theme = "system";
|
||||
|
||||
@@ -56,7 +56,7 @@ public class SetSettingCommandTests
|
||||
}
|
||||
|
||||
[DataRow(typeof(GeneralSettings), "Enabled.MouseWithoutBorders", "true")]
|
||||
[DataRow(typeof(GeneralSettings), nameof(GeneralSettings.AutoDownloadUpdates), "false")]
|
||||
[DataRow(typeof(GeneralSettings), nameof(GeneralSettings.AutoDownloadUpdates), "true")]
|
||||
[TestMethod]
|
||||
public void SetGeneralSetting(Type moduleSettingsType, string settingName, string newValueStr)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user