mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-03 18:00:25 +01:00
Compare commits
4 Commits
main
...
leilzh/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ecb496e49 | ||
|
|
4fb43e080d | ||
|
|
38c1aedc1b | ||
|
|
fca6d67a2e |
1
.github/actions/spell-check/excludes.txt
vendored
1
.github/actions/spell-check/excludes.txt
vendored
@@ -101,7 +101,6 @@
|
||||
^doc/devdocs/akaLinks\.md$
|
||||
^NOTICE\.md$
|
||||
^src/common/CalculatorEngineCommon/exprtk\.hpp$
|
||||
^src/common/UnitTests-CommonUtils/
|
||||
^src/common/ManagedCommon/ColorFormatHelper\.cs$
|
||||
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
|
||||
^src/common/sysinternals/Eula/
|
||||
|
||||
101
.github/actions/spell-check/expect.txt
vendored
101
.github/actions/spell-check/expect.txt
vendored
@@ -11,7 +11,6 @@ ACCESSDENIED
|
||||
ACCESSTOKEN
|
||||
acfs
|
||||
ACIE
|
||||
ACR
|
||||
AClient
|
||||
AColumn
|
||||
acrt
|
||||
@@ -45,7 +44,6 @@ ALLCHILDREN
|
||||
ALLINPUT
|
||||
Allman
|
||||
Allmodule
|
||||
ALLNOISE
|
||||
ALLOWUNDO
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
@@ -59,6 +57,7 @@ AOC
|
||||
aocfnapldcnfbofgmbbllojgocaelgdd
|
||||
AOklab
|
||||
aot
|
||||
APARTMENTTHREADED
|
||||
APeriod
|
||||
apicontract
|
||||
apidl
|
||||
@@ -96,7 +95,6 @@ asf
|
||||
Ashcraft
|
||||
AShortcut
|
||||
ASingle
|
||||
ASUS
|
||||
ASSOCCHANGED
|
||||
ASSOCF
|
||||
ASSOCSTR
|
||||
@@ -106,7 +104,6 @@ atl
|
||||
ATRIOX
|
||||
aumid
|
||||
authenticode
|
||||
AUO
|
||||
AUTOBUDDY
|
||||
AUTOCHECKBOX
|
||||
AUTOHIDE
|
||||
@@ -124,10 +121,6 @@ azureaiinference
|
||||
azureinference
|
||||
azureopenai
|
||||
backticks
|
||||
Backlight
|
||||
Badflags
|
||||
Badmode
|
||||
Badparam
|
||||
bbwe
|
||||
BCIE
|
||||
bck
|
||||
@@ -136,7 +129,6 @@ bezelled
|
||||
bhid
|
||||
BIF
|
||||
bigbar
|
||||
BIGGERSIZEOK
|
||||
bigobj
|
||||
binlog
|
||||
binres
|
||||
@@ -201,7 +193,6 @@ Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
CBN
|
||||
Cds
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
CCHFORMNAME
|
||||
@@ -221,7 +212,6 @@ checkmarks
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
Chunghwa
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
@@ -236,7 +226,7 @@ claude
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
clientedge
|
||||
CLIENTEDGE
|
||||
clientid
|
||||
clientside
|
||||
CLIPBOARDUPDATE
|
||||
@@ -248,7 +238,6 @@ CLSCTX
|
||||
clsids
|
||||
Clusion
|
||||
cmder
|
||||
CMN
|
||||
CMDNOTFOUNDMODULEINTERFACE
|
||||
cmdpal
|
||||
CMIC
|
||||
@@ -303,7 +292,6 @@ Corpor
|
||||
cotaskmem
|
||||
COULDNOT
|
||||
countof
|
||||
Cowait
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
@@ -322,14 +310,11 @@ CRECT
|
||||
CRH
|
||||
critsec
|
||||
cropandlock
|
||||
crt
|
||||
CROPTOSQUARE
|
||||
Crossdevice
|
||||
csdevkit
|
||||
CSearch
|
||||
CSettings
|
||||
cso
|
||||
CSOT
|
||||
CSRW
|
||||
CStyle
|
||||
cswin
|
||||
@@ -372,14 +357,11 @@ DBPROPIDSET
|
||||
DBPROPSET
|
||||
DBT
|
||||
DCBA
|
||||
DCapabilities
|
||||
DCOM
|
||||
DComposition
|
||||
DCR
|
||||
ddc
|
||||
DDEIf
|
||||
Deact
|
||||
debouncer
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
@@ -397,7 +379,6 @@ DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
DEFPUSHBUTTON
|
||||
deinitialization
|
||||
DELA
|
||||
DELETEDKEYIMAGE
|
||||
DELETESCANS
|
||||
DEMOTYPE
|
||||
@@ -432,20 +413,18 @@ DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
diskmgmt
|
||||
DISPLAYCHANGE
|
||||
displayconfig
|
||||
DISPLAYCONFIG
|
||||
DISPLAYFLAGS
|
||||
DISPLAYFREQUENCY
|
||||
displayname
|
||||
DISPLAYORIENTATION
|
||||
diu
|
||||
divyan
|
||||
Dlg
|
||||
DLGFRAME
|
||||
dlgmodalframe
|
||||
DLGMODALFRAME
|
||||
dlib
|
||||
dllhost
|
||||
dllmain
|
||||
Dmdo
|
||||
DNLEN
|
||||
DONOTROUND
|
||||
DONTVALIDATEPATH
|
||||
@@ -455,7 +434,6 @@ downsampling
|
||||
downscale
|
||||
DPICHANGED
|
||||
DPIs
|
||||
DPMS
|
||||
DPSAPI
|
||||
DQTAT
|
||||
DQTYPE
|
||||
@@ -493,19 +471,15 @@ DWMWINDOWMAXIMIZEDCHANGE
|
||||
DWORDLONG
|
||||
dworigin
|
||||
dwrite
|
||||
Dxva
|
||||
dxgi
|
||||
eab
|
||||
EAccess
|
||||
easeofaccess
|
||||
ecount
|
||||
edid
|
||||
Edid
|
||||
EDITKEYBOARD
|
||||
EDITSHORTCUTS
|
||||
EDITTEXT
|
||||
EFile
|
||||
EInvalid
|
||||
eep
|
||||
eku
|
||||
emojis
|
||||
ENABLEDELAYEDEXPANSION
|
||||
@@ -515,15 +489,14 @@ ENABLETEMPLATE
|
||||
encodedlaunch
|
||||
encryptor
|
||||
ENDSESSION
|
||||
ENot
|
||||
ENSUREVISIBLE
|
||||
ENTERSIZEMOVE
|
||||
ENTRYW
|
||||
ENU
|
||||
environmentvariables
|
||||
EOAC
|
||||
EPO
|
||||
epu
|
||||
EProvider
|
||||
ERASEBKGND
|
||||
EREOF
|
||||
EResize
|
||||
@@ -577,7 +550,6 @@ fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FFh
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -620,7 +592,6 @@ formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
Framechanged
|
||||
FRestore
|
||||
frm
|
||||
FROMTOUCH
|
||||
@@ -682,8 +653,6 @@ gwl
|
||||
GWLP
|
||||
GWLSTYLE
|
||||
hangeul
|
||||
Hann
|
||||
Hantai
|
||||
Hanzi
|
||||
Hardlines
|
||||
hardlinks
|
||||
@@ -742,7 +711,6 @@ HKPD
|
||||
HKU
|
||||
HMD
|
||||
hmenu
|
||||
HMON
|
||||
hmodule
|
||||
hmonitor
|
||||
homies
|
||||
@@ -760,7 +728,6 @@ hotkeys
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
HPhysical
|
||||
HRAWINPUT
|
||||
hredraw
|
||||
hres
|
||||
@@ -771,7 +738,6 @@ hsb
|
||||
HSCROLL
|
||||
hsi
|
||||
HSpeed
|
||||
HSync
|
||||
HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
@@ -781,7 +747,6 @@ HVal
|
||||
HValue
|
||||
Hvci
|
||||
hwb
|
||||
HWP
|
||||
HWHEEL
|
||||
HWINEVENTHOOK
|
||||
hwnd
|
||||
@@ -795,7 +760,6 @@ IAI
|
||||
icf
|
||||
ICONERROR
|
||||
ICONLOCATION
|
||||
ICONONLY
|
||||
IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
@@ -839,7 +803,6 @@ INITTOLOGFONTSTRUCT
|
||||
INLINEPREFIX
|
||||
inlines
|
||||
Inno
|
||||
Innolux
|
||||
INPC
|
||||
inproc
|
||||
INPUTHARDWARE
|
||||
@@ -881,7 +844,6 @@ istep
|
||||
ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
IVO
|
||||
IUWP
|
||||
IWIC
|
||||
jeli
|
||||
@@ -895,7 +857,6 @@ jpnime
|
||||
Jsons
|
||||
jsonval
|
||||
jxr
|
||||
Kantai
|
||||
keybd
|
||||
KEYBDDATA
|
||||
KEYBDINPUT
|
||||
@@ -917,7 +878,6 @@ KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
kvp
|
||||
KVM
|
||||
Kybd
|
||||
LARGEICON
|
||||
lastcodeanalysissucceeded
|
||||
@@ -939,9 +899,6 @@ LEFTTEXT
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
Lenovo
|
||||
LGD
|
||||
LFU
|
||||
lhwnd
|
||||
LIBFUZZER
|
||||
LIBID
|
||||
@@ -1046,7 +1003,6 @@ MAPTOSAMESHORTCUT
|
||||
MAPVK
|
||||
MARKDOWNPREVIEWHANDLERCPP
|
||||
MAXIMIZEBOX
|
||||
Maximizebox
|
||||
MAXSHORTCUTSIZE
|
||||
maxversiontested
|
||||
mber
|
||||
@@ -1059,7 +1015,6 @@ MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
mccs
|
||||
meme
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
@@ -1069,7 +1024,9 @@ MERGEPAINT
|
||||
Metacharacter
|
||||
metadatamatters
|
||||
Metadatas
|
||||
Metacharacter
|
||||
metafile
|
||||
Metacharacter
|
||||
mfc
|
||||
Mgmt
|
||||
Microwaved
|
||||
@@ -1081,7 +1038,6 @@ mikeclayton
|
||||
mindaro
|
||||
Minimizable
|
||||
MINIMIZEBOX
|
||||
Minimizebox
|
||||
MINIMIZEEND
|
||||
MINIMIZESTART
|
||||
MINMAXINFO
|
||||
@@ -1118,7 +1074,6 @@ MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
MRM
|
||||
Mrt
|
||||
mrt
|
||||
mru
|
||||
MSAL
|
||||
msc
|
||||
@@ -1144,7 +1099,6 @@ Mso
|
||||
msrc
|
||||
msstore
|
||||
mstsc
|
||||
mswhql
|
||||
msvcp
|
||||
MT
|
||||
MTND
|
||||
@@ -1162,7 +1116,6 @@ MYICON
|
||||
myorg
|
||||
myrepo
|
||||
NAMECHANGE
|
||||
Nanjing
|
||||
namespaceanddescendants
|
||||
nao
|
||||
NCACTIVATE
|
||||
@@ -1231,7 +1184,6 @@ NOMCX
|
||||
NOMINMAX
|
||||
NOMIRRORBITMAP
|
||||
NOMOVE
|
||||
Nomove
|
||||
NONANTIALIASED
|
||||
nonclient
|
||||
NONCLIENTMETRICSW
|
||||
@@ -1253,7 +1205,6 @@ NORMALUSER
|
||||
NOSEARCH
|
||||
NOSENDCHANGING
|
||||
NOSIZE
|
||||
Nosize
|
||||
NOTHOUSANDS
|
||||
NOTICKS
|
||||
NOTIFICATIONSDLL
|
||||
@@ -1261,11 +1212,9 @@ NOTIFYICONDATA
|
||||
NOTIFYICONDATAW
|
||||
NOTIMPL
|
||||
NOTOPMOST
|
||||
Notopmost
|
||||
NOTRACK
|
||||
NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
Notupdated
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
nowarn
|
||||
@@ -1309,7 +1258,6 @@ opensource
|
||||
openurl
|
||||
openxmlformats
|
||||
OPTIMIZEFORINVOKE
|
||||
Optronics
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
oss
|
||||
@@ -1345,7 +1293,6 @@ PATINVERT
|
||||
PATPAINT
|
||||
pbc
|
||||
pbi
|
||||
PBP
|
||||
PBlob
|
||||
pbrush
|
||||
pcb
|
||||
@@ -1360,7 +1307,6 @@ PDBs
|
||||
PDEVMODE
|
||||
pdisp
|
||||
PDLL
|
||||
pdmodels
|
||||
pdo
|
||||
pdto
|
||||
pdtobj
|
||||
@@ -1383,7 +1329,6 @@ pguid
|
||||
phbm
|
||||
phbmp
|
||||
phicon
|
||||
PHL
|
||||
Photoshop
|
||||
phwnd
|
||||
pici
|
||||
@@ -1416,8 +1361,6 @@ Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
POWERBROADCAST
|
||||
powerdisplay
|
||||
POWERDISPLAYMODULEINTERFACE
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1472,7 +1415,6 @@ projectname
|
||||
PROPERTYKEY
|
||||
Propset
|
||||
PROPVARIANT
|
||||
prot
|
||||
PRTL
|
||||
prvpane
|
||||
psapi
|
||||
@@ -1500,16 +1442,12 @@ PTOKEN
|
||||
PToy
|
||||
ptstr
|
||||
pui
|
||||
pvct
|
||||
PWAs
|
||||
pwcs
|
||||
PWSTR
|
||||
pwsz
|
||||
pwtd
|
||||
Qdc
|
||||
QDC
|
||||
qdc
|
||||
QDS
|
||||
qit
|
||||
QITAB
|
||||
QITABENT
|
||||
@@ -1732,7 +1670,6 @@ sigdn
|
||||
Signedness
|
||||
SIGNINGSCENARIO
|
||||
signtool
|
||||
SIIGBF
|
||||
SINGLEKEY
|
||||
sipolicy
|
||||
SIZEBOX
|
||||
@@ -1797,7 +1734,6 @@ STARTUPINFOW
|
||||
startupscreen
|
||||
STATFLAG
|
||||
STATICEDGE
|
||||
Staticedge
|
||||
staticmethod
|
||||
STATSTG
|
||||
stdafx
|
||||
@@ -1834,7 +1770,6 @@ subkeys
|
||||
sublang
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
swp
|
||||
Superbar
|
||||
sut
|
||||
svchost
|
||||
@@ -1843,8 +1778,7 @@ SVGIO
|
||||
svgz
|
||||
SVSI
|
||||
SWFO
|
||||
SWP
|
||||
Swp
|
||||
swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
@@ -1904,9 +1838,7 @@ THEMECHANGED
|
||||
themeresources
|
||||
THH
|
||||
THICKFRAME
|
||||
Thickframe
|
||||
THISCOMPONENT
|
||||
Tianma
|
||||
throughs
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
@@ -1987,13 +1919,13 @@ UNLEN
|
||||
UNORM
|
||||
unremapped
|
||||
Unsubscribes
|
||||
unsubscribes
|
||||
unvirtualized
|
||||
unwide
|
||||
unzoom
|
||||
UOffset
|
||||
UOI
|
||||
UPDATENOW
|
||||
UPDATEREGISTRY
|
||||
updown
|
||||
UPGRADINGPRODUCTCODE
|
||||
upscaling
|
||||
@@ -2020,8 +1952,6 @@ vcamp
|
||||
vcenter
|
||||
vcgtq
|
||||
VCINSTALLDIR
|
||||
vcp
|
||||
vcpname
|
||||
Vcpkg
|
||||
VCRT
|
||||
vcruntime
|
||||
@@ -2034,8 +1964,6 @@ VERIFYCONTEXT
|
||||
VERSIONINFO
|
||||
VERTRES
|
||||
VERTSIZE
|
||||
VESA
|
||||
vesa
|
||||
VFT
|
||||
vget
|
||||
vgetq
|
||||
@@ -2067,7 +1995,6 @@ VSM
|
||||
vso
|
||||
vsonline
|
||||
VSpeed
|
||||
VSync
|
||||
vstemplate
|
||||
vstest
|
||||
VSTHRD
|
||||
@@ -2109,7 +2036,7 @@ winapi
|
||||
winappsdk
|
||||
windir
|
||||
WINDOWCREATED
|
||||
windowedge
|
||||
WINDOWEDGE
|
||||
WINDOWINFO
|
||||
WINDOWNAME
|
||||
WINDOWPLACEMENT
|
||||
@@ -2133,12 +2060,12 @@ WINL
|
||||
winlogon
|
||||
winmd
|
||||
winml
|
||||
WINNT
|
||||
winres
|
||||
winrt
|
||||
winsdk
|
||||
winsta
|
||||
WINTHRESHOLD
|
||||
WINNT
|
||||
WINVER
|
||||
winxamlmanager
|
||||
withinrafael
|
||||
@@ -2150,7 +2077,6 @@ WKSG
|
||||
Wlkr
|
||||
wmain
|
||||
Wman
|
||||
wmi
|
||||
WMI
|
||||
WMICIM
|
||||
wmimgmt
|
||||
@@ -2163,7 +2089,6 @@ WNDCLASSEX
|
||||
WNDCLASSEXW
|
||||
WNDCLASSW
|
||||
WNDPROC
|
||||
Wndproc
|
||||
wnode
|
||||
wom
|
||||
WORKSPACESEDITOR
|
||||
@@ -2249,4 +2174,4 @@ Zoneszonabletester
|
||||
Zoomin
|
||||
zoomit
|
||||
ZOOMITX
|
||||
Zorder
|
||||
Zorder
|
||||
13
.github/actions/spell-check/patterns.txt
vendored
13
.github/actions/spell-check/patterns.txt
vendored
@@ -274,18 +274,5 @@ St&yle
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\bx6f677548\b
|
||||
|
||||
# Windows API constants and hardware interface terms
|
||||
\bCOINIT[_A-Z]*\b
|
||||
\bEOAC[_A-Z]*\b
|
||||
\b(?:RPC_C_AUTHN_)?WINNT\b
|
||||
\bUPDATEREGISTRY\b
|
||||
\b(?:CDS_)?UPDATEREGISTRY\b
|
||||
|
||||
# Display interface terms (HDMI, DVI, DisplayPort)
|
||||
\b(?:HDMI|DVI|DisplayPort)(?:-\d+)?\b
|
||||
|
||||
# 2D Region struct names
|
||||
\bDisplayConfig2?D?Region\b
|
||||
|
||||
# Microsoft Store URLs and product IDs
|
||||
ms-windows-store://\S+
|
||||
|
||||
@@ -210,11 +210,6 @@
|
||||
"PowerToys.PowerAccentModuleInterface.dll",
|
||||
"PowerToys.PowerAccentKeyboardService.dll",
|
||||
|
||||
"PowerToys.PowerDisplayModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
|
||||
"PowerDisplay.Lib.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerRename.exe",
|
||||
"WinUI3Apps\\PowerToys.PowerRenameContextMenu.dll",
|
||||
@@ -383,8 +378,6 @@
|
||||
"UnitsNet.dll",
|
||||
"UtfUnknown.dll",
|
||||
"Wpf.Ui.dll",
|
||||
"WmiLight.dll",
|
||||
"WmiLight.Native.dll",
|
||||
"Shmuelie.WinRTServer.dll",
|
||||
"ToolGood.Words.Pinyin.dll"
|
||||
],
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
<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="Polly.Core" Version="8.6.5" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
@@ -105,7 +104,6 @@
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom 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.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
|
||||
@@ -135,7 +133,6 @@
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="WinUIEx" Version="2.8.0" />
|
||||
<PackageVersion Include="WmiLight" Version="6.14.0" />
|
||||
<PackageVersion Include="WPF-UI" Version="3.0.5" />
|
||||
<PackageVersion Include="WyHash" Version="1.0.5" />
|
||||
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
|
||||
|
||||
32
NOTICE.md
32
NOTICE.md
@@ -10,7 +10,6 @@ This software incorporates material from third parties.
|
||||
- Installer/Runner
|
||||
- Measure tool
|
||||
- Peek
|
||||
- PowerDisplay
|
||||
- Registry Preview
|
||||
|
||||
## Utility: Color Picker
|
||||
@@ -1520,35 +1519,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## Utility: PowerDisplay
|
||||
|
||||
### Twinkle Tray
|
||||
|
||||
PowerDisplay's DDC/CI implementation references techniques from Twinkle Tray.
|
||||
|
||||
**Source**: https://github.com/xanderfrangos/twinkle-tray
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright © 2020 Xander Frangos
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## NuGet Packages used by PowerToys
|
||||
|
||||
@@ -1587,7 +1557,6 @@ SOFTWARE.
|
||||
- NLog.Extensions.Logging
|
||||
- NLog.Schema
|
||||
- OpenAI
|
||||
- Polly.Core
|
||||
- ReverseMarkdown
|
||||
- ScipBe.Common.Office.OneNote
|
||||
- SharpCompress
|
||||
@@ -1600,6 +1569,5 @@ SOFTWARE.
|
||||
- UnitsNet
|
||||
- UTF.Unknown
|
||||
- WinUIEx
|
||||
- WmiLight
|
||||
- WPF-UI
|
||||
- WyHash
|
||||
@@ -55,7 +55,6 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<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/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
</Folder>
|
||||
@@ -685,23 +684,6 @@
|
||||
<Deploy />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/Tests/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/MeasureTool/">
|
||||
<Project Path="src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj" Id="54a93af7-60c7-4f6c-99d2-fbb1f75f853a">
|
||||
<BuildDependency Project="src/common/Display/Display.vcxproj" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,223 +0,0 @@
|
||||
# MCCS Capabilities String Parser - Recursive Descent Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the recursive descent parser implementation for DDC/CI MCCS (Monitor Control Command Set) capabilities strings.
|
||||
|
||||
### Attention!
|
||||
This document and the code implement are generated by Copilot.
|
||||
|
||||
## Grammar Definition (BNF)
|
||||
|
||||
```bnf
|
||||
capabilities ::= ['('] segment* [')']
|
||||
segment ::= identifier '(' segment_content ')'
|
||||
segment_content ::= text | vcp_entries | hex_list
|
||||
vcp_entries ::= vcp_entry*
|
||||
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
|
||||
hex_list ::= hex_byte*
|
||||
hex_byte ::= [0-9A-Fa-f]{2}
|
||||
identifier ::= [a-z_A-Z]+
|
||||
text ::= [^()]+
|
||||
```
|
||||
|
||||
## Example Input
|
||||
|
||||
```
|
||||
(prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03 07)vcp(10 12 14(04 05 06) 16 60(11 12 0F) DC DF)mccs_ver(2.2)vcpname(F0(Custom Setting)))
|
||||
```
|
||||
|
||||
## Parser Architecture
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
MccsCapabilitiesParser (main parser)
|
||||
├── ParseCapabilities() → MccsParseResult
|
||||
├── ParseSegment() → ParsedSegment?
|
||||
├── ParseBalancedContent() → string
|
||||
├── ParseIdentifier() → ReadOnlySpan<char>
|
||||
├── ApplySegment() → void
|
||||
│ ├── ParseHexList() → List<byte>
|
||||
│ ├── ParseVcpEntries() → Dictionary<byte, VcpCodeInfo>
|
||||
│ └── ParseVcpNames() → void
|
||||
│
|
||||
├── VcpEntryParser (sub-parser for vcp() content)
|
||||
│ └── TryParseEntry() → VcpEntry
|
||||
│
|
||||
├── VcpNameParser (sub-parser for vcpname() content)
|
||||
│ └── TryParseEntry() → (byte code, string name)
|
||||
│
|
||||
└── WindowParser (sub-parser for windowN() content)
|
||||
├── Parse() → WindowCapability
|
||||
└── ParseSubSegment() → (name, content)?
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **ref struct for Zero Allocation**
|
||||
- Main parser uses `ref struct` to avoid heap allocation
|
||||
- Works with `ReadOnlySpan<char>` for efficient string slicing
|
||||
- No intermediate string allocations during parsing
|
||||
|
||||
2. **Recursive Descent Pattern**
|
||||
- Each grammar rule has a corresponding parse method
|
||||
- Methods call each other recursively for nested structures
|
||||
- Single-character lookahead via `Peek()`
|
||||
|
||||
3. **Error Recovery**
|
||||
- Errors are accumulated, not thrown
|
||||
- Parser attempts to continue after errors
|
||||
- Returns partial results when possible
|
||||
|
||||
4. **Sub-parsers for Specialized Content**
|
||||
- `VcpEntryParser` for VCP code entries
|
||||
- `VcpNameParser` for custom VCP names
|
||||
- Each sub-parser handles its own grammar subset
|
||||
|
||||
## Parse Methods Detail
|
||||
|
||||
### ParseCapabilities()
|
||||
Entry point. Handles optional outer parentheses and iterates through segments.
|
||||
|
||||
```csharp
|
||||
private MccsParseResult ParseCapabilities()
|
||||
{
|
||||
// Handle optional outer parens
|
||||
// while (!IsAtEnd()) { ParseSegment() }
|
||||
// Return result with accumulated errors
|
||||
}
|
||||
```
|
||||
|
||||
### ParseSegment()
|
||||
Parses a single `identifier(content)` segment.
|
||||
|
||||
```csharp
|
||||
private ParsedSegment? ParseSegment()
|
||||
{
|
||||
// 1. ParseIdentifier()
|
||||
// 2. Expect '('
|
||||
// 3. ParseBalancedContent()
|
||||
// 4. Expect ')'
|
||||
}
|
||||
```
|
||||
|
||||
### ParseBalancedContent()
|
||||
Extracts content between balanced parentheses, handling nested parens.
|
||||
|
||||
```csharp
|
||||
private string ParseBalancedContent()
|
||||
{
|
||||
int depth = 1;
|
||||
while (depth > 0) {
|
||||
if (char == '(') depth++;
|
||||
if (char == ')') depth--;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ParseVcpEntries()
|
||||
Delegates to `VcpEntryParser` for the specialized VCP entry grammar.
|
||||
|
||||
```csharp
|
||||
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
|
||||
|
||||
Examples:
|
||||
- "10" → code=0x10, values=[]
|
||||
- "14(04 05 06)" → code=0x14, values=[4, 5, 6]
|
||||
- "60(11 12 0F)" → code=0x60, values=[0x11, 0x12, 0x0F]
|
||||
```
|
||||
|
||||
## Comparison with Other Approaches
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| **Recursive Descent** (this) | Clear structure, handles nesting, extensible | More code |
|
||||
| **Regex** (DDCSharp) | Concise | Hard to debug, limited nesting |
|
||||
| **Mixed** (original) | Pragmatic | Inconsistent, hard to maintain |
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
- **Time Complexity**: O(n) where n = input length
|
||||
- **Space Complexity**: O(1) for parsing + O(m) for output where m = number of VCP codes
|
||||
- **Allocations**: Minimal - only for output structures
|
||||
|
||||
## Supported Segments
|
||||
|
||||
| Segment | Description | Parser |
|
||||
|---------|-------------|--------|
|
||||
| `prot(...)` | Protocol type | Direct assignment |
|
||||
| `type(...)` | Display type (lcd/crt) | Direct assignment |
|
||||
| `model(...)` | Model name | Direct assignment |
|
||||
| `cmds(...)` | Supported commands | ParseHexList |
|
||||
| `vcp(...)` | VCP code entries | VcpEntryParser |
|
||||
| `mccs_ver(...)` | MCCS version | Direct assignment |
|
||||
| `vcpname(...)` | Custom VCP names | VcpNameParser |
|
||||
| `windowN(...)` | PIP/PBP window capabilities | WindowParser |
|
||||
|
||||
### Window Segment Format
|
||||
|
||||
The `windowN` segment (where N is 1, 2, 3, etc.) describes PIP/PBP window capabilities:
|
||||
|
||||
```
|
||||
window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10))
|
||||
```
|
||||
|
||||
| Sub-field | Format | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `type` | `type(PIP)` or `type(PBP)` | Window type (Picture-in-Picture or Picture-by-Picture) |
|
||||
| `area` | `area(x1 y1 x2 y2)` | Window area coordinates in pixels |
|
||||
| `max` | `max(width height)` | Maximum window dimensions |
|
||||
| `min` | `min(width height)` | Minimum window dimensions |
|
||||
| `window` | `window(id)` | Window identifier |
|
||||
|
||||
All sub-fields are optional; missing fields default to zero values.
|
||||
|
||||
## Error Handling
|
||||
|
||||
```csharp
|
||||
public readonly struct ParseError
|
||||
{
|
||||
public int Position { get; } // Character position
|
||||
public string Message { get; } // Human-readable error
|
||||
}
|
||||
|
||||
public sealed class MccsParseResult
|
||||
{
|
||||
public VcpCapabilities Capabilities { get; }
|
||||
public IReadOnlyList<ParseError> Errors { get; }
|
||||
public bool HasErrors => Errors.Count > 0;
|
||||
public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```csharp
|
||||
// Parse capabilities string
|
||||
var result = MccsCapabilitiesParser.Parse(capabilitiesString);
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
var caps = result.Capabilities;
|
||||
Console.WriteLine($"Model: {caps.Model}");
|
||||
Console.WriteLine($"MCCS Version: {caps.MccsVersion}");
|
||||
Console.WriteLine($"VCP Codes: {caps.SupportedVcpCodes.Count}");
|
||||
}
|
||||
|
||||
if (result.HasErrors)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
Console.WriteLine($"Parse error at {error.Position}: {error.Message}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Edge Cases Handled
|
||||
|
||||
1. **Missing outer parentheses** (Apple Cinema Display)
|
||||
2. **No spaces between hex bytes** (`010203` vs `01 02 03`)
|
||||
3. **Nested parentheses** in VCP values
|
||||
4. **Unknown segments** (logged but not fatal)
|
||||
5. **Malformed input** (partial results returned)
|
||||
@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
}
|
||||
processes.resize(bytes / sizeof(processes[0]));
|
||||
|
||||
std::array<std::wstring_view, 45> processesToTerminate = {
|
||||
std::array<std::wstring_view, 44> processesToTerminate = {
|
||||
L"PowerToys.PowerLauncher.exe",
|
||||
L"PowerToys.Settings.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
@@ -1565,7 +1565,6 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.PowerRename.exe",
|
||||
L"PowerToys.ImageResizer.exe",
|
||||
L"PowerToys.LightSwitchService.exe",
|
||||
L"PowerToys.PowerDisplay.exe",
|
||||
L"PowerToys.GcodeThumbnailProvider.exe",
|
||||
L"PowerToys.BgcodeThumbnailProvider.exe",
|
||||
L"PowerToys.PdfThumbnailProvider.exe",
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
||||
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util" >
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define PowerDisplayAssetsFiles=?>
|
||||
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||
|
||||
<Fragment>
|
||||
<!-- Power Display -->
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -47,7 +47,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
|
||||
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
|
||||
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
|
||||
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
|
||||
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
|
||||
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
|
||||
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
|
||||
@@ -124,7 +123,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
<Compile Include="KeyboardManager.wxs" />
|
||||
<Compile Include="Peek.wxs" />
|
||||
<Compile Include="PowerRename.wxs" />
|
||||
<Compile Include="PowerDisplay.wxs" />
|
||||
<Compile Include="DscResources.wxs" />
|
||||
<Compile Include="RegistryPreview.wxs" />
|
||||
<Compile Include="Run.wxs" />
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
@@ -147,7 +146,7 @@
|
||||
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (REMOVE="ALL")" />
|
||||
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<!-- TODO: Use to activate embedded MSIX -->
|
||||
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">
|
||||
|
||||
@@ -176,10 +176,6 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
|
||||
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
|
||||
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
|
||||
|
||||
#PowerDisplay
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay"
|
||||
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
|
||||
|
||||
#New+
|
||||
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
|
||||
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs
|
||||
|
||||
@@ -45,7 +45,6 @@ namespace Common.UI
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
}
|
||||
|
||||
private static string SettingsWindowNameToString(SettingsWindow value)
|
||||
@@ -116,8 +115,6 @@ namespace Common.UI
|
||||
return "CmdPal";
|
||||
case SettingsWindow.ZoomIt:
|
||||
return "ZoomIt";
|
||||
case SettingsWindow.PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
return string.Empty;
|
||||
|
||||
@@ -32,10 +32,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredLightSwitchEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredPowerDisplayEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerDisplayEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredFancyZonesEnabledValue());
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace ManagedCommon
|
||||
PowerRename,
|
||||
PowerLauncher,
|
||||
PowerAccent,
|
||||
PowerDisplay,
|
||||
RegistryPreview,
|
||||
MeasureTool,
|
||||
ShortcutGuide,
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <appMutex.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(AppMutexTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(CreateAppMutex_ValidName_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_1";
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_SameName_ReturnsExistingHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_2";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle1.get());
|
||||
|
||||
auto handle2 = createAppMutex(mutexName);
|
||||
Assert::IsNull(handle2.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_DifferentNames_ReturnsDifferentHandles)
|
||||
{
|
||||
std::wstring mutexName1 = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_A";
|
||||
std::wstring mutexName2 = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_B";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName1);
|
||||
auto handle2 = createAppMutex(mutexName2);
|
||||
|
||||
Assert::IsNotNull(handle1.get());
|
||||
Assert::IsNotNull(handle2.get());
|
||||
Assert::AreNotEqual(handle1.get(), handle2.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_EmptyName_ReturnsHandle)
|
||||
{
|
||||
// Empty name creates unnamed mutex
|
||||
auto handle = createAppMutex(L"");
|
||||
// CreateMutexW with empty string should still work
|
||||
Assert::IsTrue(true);
|
||||
// Test passes regardless - just checking it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_LongName_ReturnsHandle)
|
||||
{
|
||||
// Create a long mutex name
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_";
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
mutexName += L"LongNameSegment";
|
||||
}
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Long names might fail, but shouldn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_SpecialCharacters_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_Special!@#$%";
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Some special characters might not be valid in mutex names
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_GlobalPrefix_ReturnsHandle)
|
||||
{
|
||||
// Global prefix for cross-session mutex
|
||||
std::wstring mutexName = L"Global\\TestMutex_" + std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Might require elevation, but shouldn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_LocalPrefix_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"Local\\TestMutex_" + std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_MultipleCalls_AllSucceed)
|
||||
{
|
||||
std::vector<wil::unique_mutex_nothrow> handles;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) +
|
||||
L"_Multi_" + std::to_wstring(i);
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
handles.push_back(std::move(handle));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_ReleaseAndRecreate_Works)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_Recreate";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle1.get());
|
||||
handle1.reset();
|
||||
|
||||
// After closing, should be able to create again
|
||||
auto handle2 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle2.get());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <color.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ColorUtilsTests)
|
||||
{
|
||||
public:
|
||||
// checkValidRGB tests
|
||||
TEST_METHOD(CheckValidRGB_ValidBlack_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#000000", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidWhite_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFFFFF", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidRed_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FF0000", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidGreen_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#00FF00", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidBlue_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#0000FF", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidMixed_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#AB12CD", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0xAB), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x12), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0xCD), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_MissingHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"FFFFFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_TooShort_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_TooLong_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFFFFFFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_InvalidChars_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#GGHHII", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_LowercaseInvalid_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#ffffff", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_EmptyString_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_OnlyHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
// checkValidARGB tests
|
||||
TEST_METHOD(CheckValidARGB_ValidBlackOpaque_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FF000000", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidWhiteOpaque_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidTransparent_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#00FFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidSemiTransparent_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#80FF0000", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x80), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidMixed_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#12345678", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x12), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x34), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x56), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x78), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_MissingHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"FFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_TooShort_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_TooLong_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_InvalidChars_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#GGHHIIJJ", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_LowercaseInvalid_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#ffffffff", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_EmptyString_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <com_object_factory.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
// Test COM object for testing the factory
|
||||
class TestComObject : public IUnknown
|
||||
{
|
||||
public:
|
||||
TestComObject() : m_refCount(1) {}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override
|
||||
{
|
||||
if (riid == IID_IUnknown)
|
||||
{
|
||||
*ppvObject = static_cast<IUnknown*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObject = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE AddRef() override
|
||||
{
|
||||
return InterlockedIncrement(&m_refCount);
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE Release() override
|
||||
{
|
||||
ULONG count = InterlockedDecrement(&m_refCount);
|
||||
if (count == 0)
|
||||
{
|
||||
delete this;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private:
|
||||
LONG m_refCount;
|
||||
};
|
||||
|
||||
TEST_CLASS(ComObjectFactoryTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ComObjectFactory_Construction_DoesNotCrash)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_IUnknown_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IUnknown* pUnknown = nullptr;
|
||||
|
||||
HRESULT hr = factory.QueryInterface(IID_IUnknown, reinterpret_cast<void**>(&pUnknown));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pUnknown);
|
||||
|
||||
if (pUnknown)
|
||||
{
|
||||
pUnknown->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_IClassFactory_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IClassFactory* pFactory = nullptr;
|
||||
|
||||
HRESULT hr = factory.QueryInterface(IID_IClassFactory, reinterpret_cast<void**>(&pFactory));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pFactory);
|
||||
|
||||
if (pFactory)
|
||||
{
|
||||
pFactory->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_InvalidInterface_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
void* pInterface = nullptr;
|
||||
|
||||
// Random GUID that we don't support
|
||||
GUID randomGuid = { 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 } };
|
||||
HRESULT hr = factory.QueryInterface(randomGuid, &pInterface);
|
||||
|
||||
Assert::AreEqual(E_NOINTERFACE, hr);
|
||||
Assert::IsNull(pInterface);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_AddRef_IncreasesRefCount)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
ULONG count1 = factory.AddRef();
|
||||
ULONG count2 = factory.AddRef();
|
||||
|
||||
Assert::IsTrue(count2 > count1);
|
||||
|
||||
// Clean up
|
||||
factory.Release();
|
||||
factory.Release();
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_Release_DecreasesRefCount)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.AddRef();
|
||||
factory.AddRef();
|
||||
ULONG count1 = factory.Release();
|
||||
ULONG count2 = factory.Release();
|
||||
|
||||
Assert::IsTrue(count2 < count1);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_NoAggregation_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IUnknown* pObj = nullptr;
|
||||
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pObj);
|
||||
|
||||
if (pObj)
|
||||
{
|
||||
pObj->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_WithAggregation_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
TestComObject outer;
|
||||
IUnknown* pObj = nullptr;
|
||||
|
||||
// Aggregation should fail for our simple test object
|
||||
HRESULT hr = factory.CreateInstance(&outer, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
|
||||
Assert::AreEqual(CLASS_E_NOAGGREGATION, hr);
|
||||
Assert::IsNull(pObj);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_NullOutput_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, nullptr);
|
||||
|
||||
Assert::AreEqual(E_POINTER, hr);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_Lock_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
HRESULT hr = factory.LockServer(TRUE);
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
|
||||
// Unlock
|
||||
factory.LockServer(FALSE);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_Unlock_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.LockServer(TRUE);
|
||||
HRESULT hr = factory.LockServer(FALSE);
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_MultipleLocks_Work)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.LockServer(TRUE);
|
||||
factory.LockServer(TRUE);
|
||||
factory.LockServer(TRUE);
|
||||
|
||||
factory.LockServer(FALSE);
|
||||
factory.LockServer(FALSE);
|
||||
HRESULT hr = factory.LockServer(FALSE);
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(ComObjectFactory_ConcurrentCreateInstance_Works)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&factory, &successCount]() {
|
||||
IUnknown* pObj = nullptr;
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
if (SUCCEEDED(hr) && pObj)
|
||||
{
|
||||
successCount++;
|
||||
pObj->Release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(10, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <elevation.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ElevationTests)
|
||||
{
|
||||
public:
|
||||
// is_process_elevated tests
|
||||
TEST_METHOD(IsProcessElevated_ReturnsBoolean)
|
||||
{
|
||||
bool result = is_process_elevated(false);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_CachedValue_ReturnsSameResult)
|
||||
{
|
||||
bool result1 = is_process_elevated(true);
|
||||
bool result2 = is_process_elevated(true);
|
||||
|
||||
// Cached value should be consistent
|
||||
Assert::AreEqual(result1, result2);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_UncachedValue_ReturnsBoolean)
|
||||
{
|
||||
bool result = is_process_elevated(false);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_CachedAndUncached_AreConsistent)
|
||||
{
|
||||
// Both should return the same value for the same process
|
||||
bool cached = is_process_elevated(true);
|
||||
bool uncached = is_process_elevated(false);
|
||||
|
||||
Assert::AreEqual(cached, uncached);
|
||||
}
|
||||
|
||||
// check_user_is_admin tests
|
||||
TEST_METHOD(CheckUserIsAdmin_ReturnsBoolean)
|
||||
{
|
||||
bool result = check_user_is_admin();
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckUserIsAdmin_ConsistentResults)
|
||||
{
|
||||
bool result1 = check_user_is_admin();
|
||||
bool result2 = check_user_is_admin();
|
||||
bool result3 = check_user_is_admin();
|
||||
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
// Relationship between elevation and admin
|
||||
TEST_METHOD(ElevationAndAdmin_Relationship)
|
||||
{
|
||||
bool elevated = is_process_elevated(false);
|
||||
bool admin = check_user_is_admin();
|
||||
(void)admin;
|
||||
|
||||
// If elevated, user should typically be admin
|
||||
// But user can be admin without process being elevated
|
||||
if (elevated)
|
||||
{
|
||||
// Elevated process usually means admin user
|
||||
// (though there are edge cases)
|
||||
}
|
||||
// Just verify both functions return without crashing
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// IsProcessOfWindowElevated tests
|
||||
TEST_METHOD(IsProcessOfWindowElevated_DesktopWindow_ReturnsBoolean)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
if (desktop)
|
||||
{
|
||||
bool result = IsProcessOfWindowElevated(desktop);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessOfWindowElevated_InvalidHwnd_DoesNotCrash)
|
||||
{
|
||||
bool result = IsProcessOfWindowElevated(nullptr);
|
||||
// Should handle null HWND gracefully
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
// ProcessInfo struct tests
|
||||
TEST_METHOD(ProcessInfo_DefaultConstruction)
|
||||
{
|
||||
ProcessInfo info{};
|
||||
Assert::AreEqual(static_cast<DWORD>(0), info.processID);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(IsProcessElevated_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
is_process_elevated(j % 2 == 0);
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// Performance of cached value
|
||||
TEST_METHOD(IsProcessElevated_CachedPerformance)
|
||||
{
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 10000; ++i)
|
||||
{
|
||||
is_process_elevated(true);
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// Cached calls should be very fast
|
||||
Assert::IsTrue(duration.count() < 1000);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <excluded_apps.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ExcludedAppsTests)
|
||||
{
|
||||
public:
|
||||
// find_app_name_in_path tests
|
||||
TEST_METHOD(FindAppNameInPath_ExactMatch_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_NoMatch_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"calc.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MultipleApps_FindsMatch)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"calc.exe", L"notepad.exe", L"word.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_EmptyApps_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = {};
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_PartialMatchInFolder_ReturnsFalse)
|
||||
{
|
||||
// "notepad" appears in folder name but not as the exe name
|
||||
std::wstring path = L"C:\\notepad\\other.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_CaseSensitive_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\NOTEPAD.EXE";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
// The function does rfind which is case-sensitive
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MatchWithDifferentExtension_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.com";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MatchAtEndOfPath_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_UNCPath_Works)
|
||||
{
|
||||
std::wstring path = L"\\\\server\\share\\folder\\app.exe";
|
||||
std::vector<std::wstring> apps = { L"app.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
// find_folder_in_path tests
|
||||
TEST_METHOD(FindFolderInPath_FolderExists_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\MyApp\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_FolderNotExists_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_MultipleFolders_FindsMatch)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files", L"System32", L"Users" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"";
|
||||
std::vector<std::wstring> folders = { L"Windows" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_EmptyFolders_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\app.exe";
|
||||
std::vector<std::wstring> folders = {};
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_PartialMatch_ReturnsTrue)
|
||||
{
|
||||
// find_folder_in_path uses rfind which finds substrings
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"System" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_NestedFolder_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\Company\\Product\\bin\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Product" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_RootDrive_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\folder\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"C:\\" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_UNCPath_Works)
|
||||
{
|
||||
std::wstring path = L"\\\\server\\share\\folder\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"share" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_CaseSensitive_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\WINDOWS\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"windows" };
|
||||
// rfind is case-sensitive
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
// Edge case tests
|
||||
TEST_METHOD(FindAppNameInPath_AppNameInMiddleOfPath_HandlesCorrectly)
|
||||
{
|
||||
// The app name appears both in folder and as filename
|
||||
std::wstring path = L"C:\\notepad\\bin\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_JustFilename_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
// find_app_name_in_path expects a path separator to validate the executable segment
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_JustFilename_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"app.exe";
|
||||
std::vector<std::wstring> folders = { L"Windows" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <exec.h>
|
||||
#include <cctype>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ExecTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ExecAndReadOutput_EchoCommand_ReturnsOutput)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo hello", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Output should contain "hello"
|
||||
Assert::IsTrue(result->find("hello") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_WhereCommand_ReturnsPath)
|
||||
{
|
||||
auto result = exec_and_read_output(L"where cmd", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Should contain path to cmd.exe
|
||||
Assert::IsTrue(result->find("cmd") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_DirCommand_ReturnsListing)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c dir /b C:\\Windows", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Should contain some common Windows folder names
|
||||
std::string output = *result;
|
||||
std::transform(output.begin(), output.end(), output.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
Assert::IsTrue(output.find("system32") != std::string::npos ||
|
||||
output.find("system") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_InvalidCommand_ReturnsEmptyOrError)
|
||||
{
|
||||
auto result = exec_and_read_output(L"nonexistentcommand12345", 5000);
|
||||
|
||||
// Invalid command should either return nullopt or an error message
|
||||
Assert::IsTrue(!result.has_value() || result->empty() ||
|
||||
result->find("not recognized") != std::string::npos ||
|
||||
result->find("error") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_EmptyCommand_DoesNotCrash)
|
||||
{
|
||||
auto result = exec_and_read_output(L"", 5000);
|
||||
// Should handle empty command gracefully
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_TimeoutExpires_ReturnsAvailableOutput)
|
||||
{
|
||||
// Use a command that produces output slowly
|
||||
// ping localhost will run for a while
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Very short timeout
|
||||
auto result = exec_and_read_output(L"ping localhost -n 10", 100);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should return within reasonable time
|
||||
Assert::IsTrue(elapsed.count() < 5000);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_MultilineOutput_PreservesLines)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c \"echo line1 & echo line2 & echo line3\"", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
// Should contain multiple lines
|
||||
Assert::IsTrue(result->find("line1") != std::string::npos);
|
||||
Assert::IsTrue(result->find("line2") != std::string::npos);
|
||||
Assert::IsTrue(result->find("line3") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_UnicodeOutput_Works)
|
||||
{
|
||||
// Echo a simple ASCII string (Unicode test depends on system codepage)
|
||||
auto result = exec_and_read_output(L"cmd /c echo test123", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("test123") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_LongTimeout_Works)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo test", 60000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("test") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_QuotedArguments_Work)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo \"hello world\"", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("hello") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_EnvironmentVariable_Expanded)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo %USERNAME%", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
// Should not contain the literal %USERNAME% but the actual username
|
||||
// Or if not expanded, still should not crash
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_ExitCode_CommandFails)
|
||||
{
|
||||
// Command that exits with error
|
||||
auto result = exec_and_read_output(L"cmd /c exit 1", 5000);
|
||||
|
||||
// Should still return something (possibly empty)
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_ZeroTimeout_DoesNotHang)
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
auto result = exec_and_read_output(L"cmd /c echo test", 0);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should complete quickly with zero timeout
|
||||
Assert::IsTrue(elapsed.count() < 5000);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <game_mode.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(GameModeTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(DetectGameMode_ReturnsBoolean)
|
||||
{
|
||||
// This function queries Windows game mode status
|
||||
bool result = detect_game_mode();
|
||||
|
||||
// Result depends on current system state, but should be a valid boolean
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_ConsistentResults)
|
||||
{
|
||||
// Multiple calls should return consistent results (unless game mode changes)
|
||||
bool result1 = detect_game_mode();
|
||||
bool result2 = detect_game_mode();
|
||||
bool result3 = detect_game_mode();
|
||||
|
||||
// Results should be consistent across rapid calls
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_DoesNotCrash)
|
||||
{
|
||||
// Call multiple times to ensure no crash or memory leak
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
detect_game_mode();
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_ThreadSafe)
|
||||
{
|
||||
// Test that calling from multiple threads is safe
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
detect_game_mode();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <gpo.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace powertoys_gpo;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(GpoTests)
|
||||
{
|
||||
public:
|
||||
// Helper to check if result is a valid gpo_rule_configured_t value
|
||||
static constexpr bool IsValidGpoResult(gpo_rule_configured_t result)
|
||||
{
|
||||
return result == gpo_rule_configured_wrong_value ||
|
||||
result == gpo_rule_configured_unavailable ||
|
||||
result == gpo_rule_configured_not_configured ||
|
||||
result == gpo_rule_configured_disabled ||
|
||||
result == gpo_rule_configured_enabled;
|
||||
}
|
||||
|
||||
// gpo_rule_configured_t enum tests
|
||||
TEST_METHOD(GpoRuleConfigured_EnumValues_AreDistinct)
|
||||
{
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_not_configured),
|
||||
static_cast<int>(gpo_rule_configured_enabled));
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_enabled),
|
||||
static_cast<int>(gpo_rule_configured_disabled));
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_not_configured),
|
||||
static_cast<int>(gpo_rule_configured_disabled));
|
||||
}
|
||||
|
||||
// getConfiguredValue tests
|
||||
TEST_METHOD(GetConfiguredValue_NonExistentKey_ReturnsNotConfigured)
|
||||
{
|
||||
auto result = getConfiguredValue(L"NonExistentPolicyValue12345");
|
||||
Assert::IsTrue(result == gpo_rule_configured_not_configured ||
|
||||
result == gpo_rule_configured_unavailable);
|
||||
}
|
||||
|
||||
// Utility enabled getters - these all follow the same pattern
|
||||
TEST_METHOD(GetAllowExperimentationValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getAllowExperimentationValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAlwaysOnTopEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAlwaysOnTopEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAwakeEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAwakeEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredColorPickerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredColorPickerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFancyZonesEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFancyZonesEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFileLocksmithEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFileLocksmithEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredImageResizerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredImageResizerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredKeyboardManagerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredKeyboardManagerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPowerRenameEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPowerRenameEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPowerLauncherEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPowerLauncherEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredShortcutGuideEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredShortcutGuideEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredTextExtractorEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredTextExtractorEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredHostsFileEditorEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredHostsFileEditorEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMousePointerCrosshairsEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMousePointerCrosshairsEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseHighlighterEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseHighlighterEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseJumpEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseJumpEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFindMyMouseEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFindMyMouseEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseWithoutBordersEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseWithoutBordersEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAdvancedPasteEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAdvancedPasteEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPeekEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPeekEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredRegistryPreviewEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredRegistryPreviewEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredScreenRulerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredScreenRulerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredCropAndLockEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredCropAndLockEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredEnvironmentVariablesEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredEnvironmentVariablesEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
// All GPO functions should not crash
|
||||
TEST_METHOD(AllGpoFunctions_DoNotCrash)
|
||||
{
|
||||
getAllowExperimentationValue();
|
||||
getConfiguredAlwaysOnTopEnabledValue();
|
||||
getConfiguredAwakeEnabledValue();
|
||||
getConfiguredColorPickerEnabledValue();
|
||||
getConfiguredFancyZonesEnabledValue();
|
||||
getConfiguredFileLocksmithEnabledValue();
|
||||
getConfiguredImageResizerEnabledValue();
|
||||
getConfiguredKeyboardManagerEnabledValue();
|
||||
getConfiguredPowerRenameEnabledValue();
|
||||
getConfiguredPowerLauncherEnabledValue();
|
||||
getConfiguredShortcutGuideEnabledValue();
|
||||
getConfiguredTextExtractorEnabledValue();
|
||||
getConfiguredHostsFileEditorEnabledValue();
|
||||
getConfiguredMousePointerCrosshairsEnabledValue();
|
||||
getConfiguredMouseHighlighterEnabledValue();
|
||||
getConfiguredMouseJumpEnabledValue();
|
||||
getConfiguredFindMyMouseEnabledValue();
|
||||
getConfiguredMouseWithoutBordersEnabledValue();
|
||||
getConfiguredAdvancedPasteEnabledValue();
|
||||
getConfiguredPeekEnabledValue();
|
||||
getConfiguredRegistryPreviewEnabledValue();
|
||||
getConfiguredScreenRulerEnabledValue();
|
||||
getConfiguredCropAndLockEnabledValue();
|
||||
getConfiguredEnvironmentVariablesEnabledValue();
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <HDropIterator.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(HDropIteratorTests)
|
||||
{
|
||||
public:
|
||||
// Helper to create a test HDROP structure
|
||||
static HGLOBAL CreateTestHDrop(const std::vector<std::wstring>& files)
|
||||
{
|
||||
// Calculate required size
|
||||
size_t size = sizeof(DROPFILES);
|
||||
for (const auto& file : files)
|
||||
{
|
||||
size += (file.length() + 1) * sizeof(wchar_t);
|
||||
}
|
||||
size += sizeof(wchar_t); // Double null terminator
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GHND, size);
|
||||
if (!hGlobal) return nullptr;
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
if (!pDropFiles)
|
||||
{
|
||||
GlobalFree(hGlobal);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
pDropFiles->pFiles = sizeof(DROPFILES);
|
||||
pDropFiles->fWide = TRUE;
|
||||
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + sizeof(DROPFILES));
|
||||
for (const auto& file : files)
|
||||
{
|
||||
wcscpy_s(pData, file.length() + 1, file.c_str());
|
||||
pData += file.length() + 1;
|
||||
}
|
||||
*pData = L'\0'; // Double null terminator
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
return hGlobal;
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_EmptyDrop_IsDoneImmediately)
|
||||
{
|
||||
HGLOBAL hGlobal = CreateTestHDrop({});
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true); // Skip if allocation failed
|
||||
return;
|
||||
}
|
||||
|
||||
STGMEDIUM medium = {};
|
||||
medium.tymed = TYMED_HGLOBAL;
|
||||
medium.hGlobal = hGlobal;
|
||||
|
||||
// Without a proper IDataObject, we can't fully test
|
||||
// Just verify the class can be instantiated conceptually
|
||||
GlobalFree(hGlobal);
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_Iteration_Conceptual)
|
||||
{
|
||||
// This test verifies the concept of iteration
|
||||
// Full integration testing requires a proper IDataObject
|
||||
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\test\\file1.txt",
|
||||
L"C:\\test\\file2.txt",
|
||||
L"C:\\test\\file3.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify we can create the HDROP structure
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
Assert::IsTrue(pDropFiles->fWide);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_SingleFile_Works)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = { L"C:\\test\\single.txt" };
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify structure
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
// Read back the file name
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
Assert::AreEqual(std::wstring(L"C:\\test\\single.txt"), std::wstring(pData));
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_MultipleFiles_Structure)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\file1.txt",
|
||||
L"C:\\file2.txt",
|
||||
L"C:\\file3.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
// Count files by iterating through null-terminated strings
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
int count = 0;
|
||||
while (*pData)
|
||||
{
|
||||
count++;
|
||||
pData += wcslen(pData) + 1;
|
||||
}
|
||||
|
||||
Assert::AreEqual(3, count);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_UnicodeFilenames_Work)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\test\\file.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsTrue(pDropFiles->fWide == TRUE);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_LongFilenames_Work)
|
||||
{
|
||||
std::wstring longPath = L"C:\\";
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
longPath += L"LongFolderName\\";
|
||||
}
|
||||
longPath += L"file.txt";
|
||||
|
||||
std::vector<std::wstring> testFiles = { longPath };
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
Assert::AreEqual(longPath, std::wstring(pData));
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <HttpClient.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(HttpClientTests)
|
||||
{
|
||||
public:
|
||||
// Note: Network tests may fail in offline environments
|
||||
// These tests are designed to verify the API doesn't crash
|
||||
|
||||
TEST_METHOD(HttpClient_DefaultConstruction)
|
||||
{
|
||||
http::HttpClient client;
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Request_InvalidUri_ReturnsEmpty)
|
||||
{
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
// Invalid URI should not crash, may throw or return empty
|
||||
auto result = client.request(winrt::Windows::Foundation::Uri(L"invalid://not-a-valid-uri"));
|
||||
// If we get here, result may be empty or contain error
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for invalid URI
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_InvalidUri_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
TestHelpers::TempFile tempFile;
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://invalid.invalid.invalid"),
|
||||
tempFile.path());
|
||||
// May return false or throw
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for invalid/unreachable URI
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_WithCallback_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
TestHelpers::TempFile tempFile;
|
||||
std::atomic<int> callbackCount{ 0 };
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://invalid.invalid.invalid"),
|
||||
tempFile.path(),
|
||||
[&callbackCount]([[maybe_unused]] float progress) {
|
||||
callbackCount++;
|
||||
});
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_EmptyPath_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://example.com"),
|
||||
L"");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for empty path
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// These tests require network access and may be skipped in offline environments
|
||||
TEST_METHOD(HttpClient_Request_ValidUri_ReturnsResult)
|
||||
{
|
||||
// Skip this test in most CI environments
|
||||
// Only run manually to verify network functionality
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
// Use a reliable, fast-responding URL
|
||||
auto result = client.request(winrt::Windows::Foundation::Uri(L"https://www.microsoft.com"));
|
||||
// Result may or may not be successful depending on network
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Network errors are acceptable in test environment
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety test (doesn't require network)
|
||||
TEST_METHOD(HttpClient_MultipleInstances_DoNotCrash)
|
||||
{
|
||||
std::vector<std::unique_ptr<http::HttpClient>> clients;
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
clients.push_back(std::make_unique<http::HttpClient>());
|
||||
}
|
||||
|
||||
// All clients should coexist without crashing
|
||||
Assert::AreEqual(static_cast<size_t>(10), clients.size());
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_ConcurrentConstruction_DoesNotCrash)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
http::HttpClient client;
|
||||
successCount++;
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(5, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <json.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace winrt::Windows::Data::Json;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(JsonTests)
|
||||
{
|
||||
public:
|
||||
// from_file tests
|
||||
TEST_METHOD(FromFile_NonExistentFile_ReturnsNullopt)
|
||||
{
|
||||
auto result = json::from_file(L"C:\\NonExistent\\File\\Path.json");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_ValidJsonFile_ReturnsJsonObject)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("{\"key\": \"value\"}");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_InvalidJson_ReturnsNullopt)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("not valid json {{{");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_EmptyFile_ReturnsNullopt)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
// File is empty
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_ValidComplexJson_ParsesCorrectly)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("{\"name\": \"test\", \"value\": 42, \"enabled\": true}");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
|
||||
auto& obj = *result;
|
||||
Assert::IsTrue(obj.HasKey(L"name"));
|
||||
Assert::IsTrue(obj.HasKey(L"value"));
|
||||
Assert::IsTrue(obj.HasKey(L"enabled"));
|
||||
}
|
||||
|
||||
// to_file tests
|
||||
TEST_METHOD(ToFile_ValidObject_WritesFile)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
json::to_file(tempFile.path(), obj);
|
||||
|
||||
// Read back and verify
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->HasKey(L"key"));
|
||||
}
|
||||
|
||||
TEST_METHOD(ToFile_ComplexObject_WritesFile)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"name", JsonValue::CreateStringValue(L"test"));
|
||||
obj.SetNamedValue(L"value", JsonValue::CreateNumberValue(42));
|
||||
obj.SetNamedValue(L"enabled", JsonValue::CreateBooleanValue(true));
|
||||
json::to_file(tempFile.path(), obj);
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(std::wstring(L"test"), std::wstring(result->GetNamedString(L"name")));
|
||||
Assert::AreEqual(42.0, result->GetNamedNumber(L"value"));
|
||||
Assert::IsTrue(result->GetNamedBoolean(L"enabled"));
|
||||
}
|
||||
|
||||
// has tests
|
||||
TEST_METHOD(Has_ExistingKey_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::String));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_NonExistingKey_ReturnsFalse)
|
||||
{
|
||||
JsonObject obj;
|
||||
Assert::IsFalse(json::has(obj, L"key", JsonValueType::String));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_WrongType_ReturnsFalse)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
Assert::IsFalse(json::has(obj, L"key", JsonValueType::Number));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_NumberType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateNumberValue(42));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Number));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_BooleanType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateBooleanValue(true));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Boolean));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_ObjectType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
JsonObject nested;
|
||||
obj.SetNamedValue(L"key", nested);
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Object));
|
||||
}
|
||||
|
||||
// value function tests
|
||||
TEST_METHOD(Value_IntegerType_CreatesNumberValue)
|
||||
{
|
||||
auto val = json::value(42);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Number);
|
||||
Assert::AreEqual(42.0, val.GetNumber());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_DoubleType_CreatesNumberValue)
|
||||
{
|
||||
auto val = json::value(3.14);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Number);
|
||||
Assert::AreEqual(3.14, val.GetNumber());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_BooleanTrue_CreatesBooleanValue)
|
||||
{
|
||||
auto val = json::value(true);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Boolean);
|
||||
Assert::IsTrue(val.GetBoolean());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_BooleanFalse_CreatesBooleanValue)
|
||||
{
|
||||
auto val = json::value(false);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Boolean);
|
||||
Assert::IsFalse(val.GetBoolean());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_String_CreatesStringValue)
|
||||
{
|
||||
auto val = json::value(L"hello");
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::String);
|
||||
Assert::AreEqual(std::wstring(L"hello"), std::wstring(val.GetString()));
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_JsonObject_ReturnsJsonValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
auto val = json::value(obj);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Object);
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_JsonValue_ReturnsIdentity)
|
||||
{
|
||||
auto original = JsonValue::CreateStringValue(L"test");
|
||||
auto result = json::value(original);
|
||||
Assert::AreEqual(std::wstring(L"test"), std::wstring(result.GetString()));
|
||||
}
|
||||
|
||||
// get function tests
|
||||
TEST_METHOD(Get_BooleanValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"enabled", JsonValue::CreateBooleanValue(true));
|
||||
|
||||
bool result = false;
|
||||
json::get(obj, L"enabled", result);
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_IntValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"count", JsonValue::CreateNumberValue(42));
|
||||
|
||||
int result = 0;
|
||||
json::get(obj, L"count", result);
|
||||
Assert::AreEqual(42, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_DoubleValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"ratio", JsonValue::CreateNumberValue(3.14));
|
||||
|
||||
double result = 0.0;
|
||||
json::get(obj, L"ratio", result);
|
||||
Assert::AreEqual(3.14, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_StringValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"name", JsonValue::CreateStringValue(L"test"));
|
||||
|
||||
std::wstring result;
|
||||
json::get(obj, L"name", result);
|
||||
Assert::AreEqual(std::wstring(L"test"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_MissingKey_UsesDefault)
|
||||
{
|
||||
JsonObject obj;
|
||||
|
||||
int result = 0;
|
||||
json::get(obj, L"missing", result, 99);
|
||||
Assert::AreEqual(99, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_MissingKeyNoDefault_PreservesOriginal)
|
||||
{
|
||||
JsonObject obj;
|
||||
|
||||
int result = 42;
|
||||
json::get(obj, L"missing", result);
|
||||
// When key is missing and no default, original value is preserved
|
||||
Assert::AreEqual(42, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_JsonObject_ReturnsObject)
|
||||
{
|
||||
JsonObject obj;
|
||||
JsonObject nested;
|
||||
nested.SetNamedValue(L"inner", JsonValue::CreateStringValue(L"value"));
|
||||
obj.SetNamedValue(L"nested", nested);
|
||||
|
||||
JsonObject result;
|
||||
json::get(obj, L"nested", result);
|
||||
Assert::IsTrue(result.HasKey(L"inner"));
|
||||
}
|
||||
|
||||
// Roundtrip tests
|
||||
TEST_METHOD(Roundtrip_ComplexObject_PreservesData)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject original;
|
||||
original.SetNamedValue(L"string", JsonValue::CreateStringValue(L"hello"));
|
||||
original.SetNamedValue(L"number", JsonValue::CreateNumberValue(42));
|
||||
original.SetNamedValue(L"boolean", JsonValue::CreateBooleanValue(true));
|
||||
|
||||
JsonObject nested;
|
||||
nested.SetNamedValue(L"inner", JsonValue::CreateStringValue(L"world"));
|
||||
original.SetNamedValue(L"object", nested);
|
||||
|
||||
json::to_file(tempFile.path(), original);
|
||||
auto loaded = json::from_file(tempFile.path());
|
||||
|
||||
Assert::IsTrue(loaded.has_value());
|
||||
Assert::AreEqual(std::wstring(L"hello"), std::wstring(loaded->GetNamedString(L"string")));
|
||||
Assert::AreEqual(42.0, loaded->GetNamedNumber(L"number"));
|
||||
Assert::IsTrue(loaded->GetNamedBoolean(L"boolean"));
|
||||
Assert::AreEqual(std::wstring(L"world"), std::wstring(loaded->GetNamedObject(L"object").GetNamedString(L"inner")));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <logger_helper.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace LoggerHelpers;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(LoggerHelperTests)
|
||||
{
|
||||
public:
|
||||
// get_log_folder_path tests
|
||||
TEST_METHOD(GetLogFolderPath_ValidAppPath_ReturnsPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"TestApp");
|
||||
|
||||
Assert::IsFalse(result.empty());
|
||||
// Should contain the app name or be a valid path
|
||||
auto pathStr = result.wstring();
|
||||
Assert::IsTrue(pathStr.length() > 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_EmptyAppPath_ReturnsPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"");
|
||||
|
||||
// Should still return a base path
|
||||
Assert::IsTrue(true); // Just verify no crash
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_SpecialCharacters_Works)
|
||||
{
|
||||
auto result = get_log_folder_path(L"Test App With Spaces");
|
||||
|
||||
// Should handle spaces in path
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_ConsistentResults)
|
||||
{
|
||||
auto result1 = get_log_folder_path(L"TestApp");
|
||||
auto result2 = get_log_folder_path(L"TestApp");
|
||||
|
||||
Assert::AreEqual(result1.wstring(), result2.wstring());
|
||||
}
|
||||
|
||||
// dir_exists tests
|
||||
TEST_METHOD(DirExists_WindowsDirectory_ReturnsTrue)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\Windows"));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_NonExistentDirectory_ReturnsFalse)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\NonExistentDir12345"));
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_FileInsteadOfDir_ReturnsTrue)
|
||||
{
|
||||
// notepad.exe is a file, not a directory
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\Windows\\notepad.exe"));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L""));
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_TempDirectory_ReturnsTrue)
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
bool result = dir_exists(std::filesystem::path(tempPath));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
// delete_old_log_folder tests
|
||||
TEST_METHOD(DeleteOldLogFolder_NonExistentFolder_DoesNotCrash)
|
||||
{
|
||||
delete_old_log_folder(std::filesystem::path(L"C:\\NonExistentLogFolder12345"));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DeleteOldLogFolder_ValidEmptyFolder_Works)
|
||||
{
|
||||
TestHelpers::TempDirectory tempDir;
|
||||
|
||||
// Create a subfolder structure
|
||||
auto logFolder = std::filesystem::path(tempDir.path()) / L"logs";
|
||||
std::filesystem::create_directories(logFolder);
|
||||
|
||||
Assert::IsTrue(std::filesystem::exists(logFolder));
|
||||
|
||||
delete_old_log_folder(logFolder);
|
||||
|
||||
// Folder may or may not be deleted depending on implementation
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// delete_other_versions_log_folders tests
|
||||
TEST_METHOD(DeleteOtherVersionsLogFolders_NonExistentPath_DoesNotCrash)
|
||||
{
|
||||
delete_other_versions_log_folders(L"C:\\NonExistent\\Path", L"1.0.0");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DeleteOtherVersionsLogFolders_EmptyVersion_DoesNotCrash)
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
delete_other_versions_log_folders(tempPath, L"");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(GetLogFolderPath_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount, i]() {
|
||||
auto path = get_log_folder_path(L"TestApp" + std::to_wstring(i));
|
||||
if (!path.empty())
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(10, successCount.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
dir_exists(std::filesystem::path(L"C:\\Windows"));
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// Path construction tests
|
||||
TEST_METHOD(GetLogFolderPath_ReturnsValidFilesystemPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"TestApp");
|
||||
|
||||
// Should be a valid path that we can use with filesystem operations
|
||||
Assert::IsTrue(result.is_absolute() || result.has_root_name() || !result.empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <modulesRegistry.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
static std::wstring GetInstallDir()
|
||||
{
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
return std::filesystem::path{ path }.parent_path().wstring();
|
||||
}
|
||||
|
||||
TEST_CLASS(ModulesRegistryTests)
|
||||
{
|
||||
public:
|
||||
// Test that all changeset generator functions return valid changesets
|
||||
TEST_METHOD(GetSvgPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetSvgThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getSvgThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMarkdownPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getMdPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMonacoPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getMonacoPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetPdfPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getPdfPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetPdfThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getPdfThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetGcodePreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getGcodePreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetGcodeThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getGcodeThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetStlThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getStlThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetQoiPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getQoiPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetQoiThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getQoiThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test enabled vs disabled state
|
||||
TEST_METHOD(ChangeSet_EnabledVsDisabled_MayDiffer)
|
||||
{
|
||||
auto enabledSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), true);
|
||||
auto disabledSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
// Both should be valid change sets
|
||||
Assert::IsFalse(enabledSet.changes.empty());
|
||||
Assert::IsFalse(disabledSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test getAllOnByDefaultModulesChangeSets
|
||||
TEST_METHOD(GetAllOnByDefaultModulesChangeSets_ReturnsMultipleChangeSets)
|
||||
{
|
||||
auto changeSets = getAllOnByDefaultModulesChangeSets(GetInstallDir());
|
||||
|
||||
// Should return multiple changesets for all default-enabled modules
|
||||
Assert::IsTrue(changeSets.size() > 0);
|
||||
}
|
||||
|
||||
// Test getAllModulesChangeSets
|
||||
TEST_METHOD(GetAllModulesChangeSets_ReturnsChangeSets)
|
||||
{
|
||||
auto changeSets = getAllModulesChangeSets(GetInstallDir());
|
||||
|
||||
// Should return changesets for all modules
|
||||
Assert::IsTrue(changeSets.size() > 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAllModulesChangeSets_ContainsMoreThanOnByDefault)
|
||||
{
|
||||
auto allSets = getAllModulesChangeSets(GetInstallDir());
|
||||
auto defaultSets = getAllOnByDefaultModulesChangeSets(GetInstallDir());
|
||||
|
||||
// All modules should be >= on-by-default modules
|
||||
Assert::IsTrue(allSets.size() >= defaultSets.size());
|
||||
}
|
||||
|
||||
// Test that changesets have valid structure
|
||||
TEST_METHOD(ChangeSet_HasValidKeyPath)
|
||||
{
|
||||
auto changeSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test all changeset functions don't crash
|
||||
TEST_METHOD(AllChangeSetFunctions_DoNotCrash)
|
||||
{
|
||||
auto installDir = GetInstallDir();
|
||||
getSvgPreviewHandlerChangeSet(installDir, true);
|
||||
getSvgPreviewHandlerChangeSet(installDir, false);
|
||||
getSvgThumbnailHandlerChangeSet(installDir, true);
|
||||
getSvgThumbnailHandlerChangeSet(installDir, false);
|
||||
getMdPreviewHandlerChangeSet(installDir, true);
|
||||
getMdPreviewHandlerChangeSet(installDir, false);
|
||||
getMonacoPreviewHandlerChangeSet(installDir, true);
|
||||
getMonacoPreviewHandlerChangeSet(installDir, false);
|
||||
getPdfPreviewHandlerChangeSet(installDir, true);
|
||||
getPdfPreviewHandlerChangeSet(installDir, false);
|
||||
getPdfThumbnailHandlerChangeSet(installDir, true);
|
||||
getPdfThumbnailHandlerChangeSet(installDir, false);
|
||||
getGcodePreviewHandlerChangeSet(installDir, true);
|
||||
getGcodePreviewHandlerChangeSet(installDir, false);
|
||||
getGcodeThumbnailHandlerChangeSet(installDir, true);
|
||||
getGcodeThumbnailHandlerChangeSet(installDir, false);
|
||||
getStlThumbnailHandlerChangeSet(installDir, true);
|
||||
getStlThumbnailHandlerChangeSet(installDir, false);
|
||||
getQoiPreviewHandlerChangeSet(installDir, true);
|
||||
getQoiPreviewHandlerChangeSet(installDir, false);
|
||||
getQoiThumbnailHandlerChangeSet(installDir, true);
|
||||
getQoiThumbnailHandlerChangeSet(installDir, false);
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <MsWindowsSettings.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(MsWindowsSettingsTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(GetAnimationsEnabled_ReturnsBoolean)
|
||||
{
|
||||
bool result = GetAnimationsEnabled();
|
||||
|
||||
// Should return a valid boolean
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAnimationsEnabled_ConsistentResults)
|
||||
{
|
||||
// Multiple calls should return consistent results
|
||||
bool result1 = GetAnimationsEnabled();
|
||||
bool result2 = GetAnimationsEnabled();
|
||||
bool result3 = GetAnimationsEnabled();
|
||||
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAnimationsEnabled_DoesNotCrash)
|
||||
{
|
||||
// Call multiple times to ensure stability
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
GetAnimationsEnabled();
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAnimationsEnabled_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
GetAnimationsEnabled();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <MsiUtils.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(MsiUtilsTests)
|
||||
{
|
||||
public:
|
||||
// GetMsiPackageInstalledPath tests
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_PerUser_DoesNotCrash)
|
||||
{
|
||||
auto result = GetMsiPackageInstalledPath(true);
|
||||
// Result depends on installation state, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_PerMachine_DoesNotCrash)
|
||||
{
|
||||
auto result = GetMsiPackageInstalledPath(false);
|
||||
// Result depends on installation state, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_ConsistentResults)
|
||||
{
|
||||
auto result1 = GetMsiPackageInstalledPath(true);
|
||||
auto result2 = GetMsiPackageInstalledPath(true);
|
||||
|
||||
// Results should be consistent
|
||||
Assert::AreEqual(result1.has_value(), result2.has_value());
|
||||
if (result1.has_value() && result2.has_value())
|
||||
{
|
||||
Assert::AreEqual(*result1, *result2);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_PerUserVsPerMachine_MayDiffer)
|
||||
{
|
||||
auto perUser = GetMsiPackageInstalledPath(true);
|
||||
auto perMachine = GetMsiPackageInstalledPath(false);
|
||||
|
||||
// These may or may not be equal depending on installation
|
||||
// Just verify they don't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// GetMsiPackagePath tests
|
||||
TEST_METHOD(GetMsiPackagePath_DoesNotCrash)
|
||||
{
|
||||
auto result = GetMsiPackagePath();
|
||||
// Result depends on installation state, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackagePath_ConsistentResults)
|
||||
{
|
||||
auto result1 = GetMsiPackagePath();
|
||||
auto result2 = GetMsiPackagePath();
|
||||
|
||||
// Results should be consistent
|
||||
Assert::AreEqual(result1, result2);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 5; ++j)
|
||||
{
|
||||
GetMsiPackageInstalledPath(j % 2 == 0);
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(25, successCount.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackagePath_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 5; ++j)
|
||||
{
|
||||
GetMsiPackagePath();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(25, successCount.load());
|
||||
}
|
||||
|
||||
// Return value format tests
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_ReturnsValidPathOrEmpty)
|
||||
{
|
||||
auto path = GetMsiPackageInstalledPath(true);
|
||||
|
||||
if (path.has_value() && !path->empty())
|
||||
{
|
||||
// If a path is returned, it should contain backslash or be a valid path format
|
||||
Assert::IsTrue(path->find(L'\\') != std::wstring::npos ||
|
||||
path->find(L'/') != std::wstring::npos ||
|
||||
path->length() >= 2); // At minimum drive letter + colon
|
||||
}
|
||||
// No value or empty is also valid (not installed)
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackagePath_ReturnsValidPathOrEmpty)
|
||||
{
|
||||
auto path = GetMsiPackagePath();
|
||||
|
||||
if (!path.empty())
|
||||
{
|
||||
// If a path is returned, it should be a valid path format
|
||||
Assert::IsTrue(path.find(L'\\') != std::wstring::npos ||
|
||||
path.find(L'/') != std::wstring::npos ||
|
||||
path.length() >= 2);
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <os-detect.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(OsDetectTests)
|
||||
{
|
||||
public:
|
||||
// IsAPIContractVxAvailable tests
|
||||
TEST_METHOD(IsAPIContractV8Available_ReturnsBoolean)
|
||||
{
|
||||
// This test verifies the function runs without crashing
|
||||
// The actual result depends on the OS version
|
||||
bool result = IsAPIContractV8Available();
|
||||
// Result is either true or false, both are valid
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsAPIContractVxAvailable_V1_ReturnsTrue)
|
||||
{
|
||||
// API contract v1 should be available on any modern Windows
|
||||
bool result = IsAPIContractVxAvailable<1>();
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsAPIContractVxAvailable_V5_ReturnsBooleanConsistently)
|
||||
{
|
||||
// Call multiple times to verify caching works correctly
|
||||
bool result1 = IsAPIContractVxAvailable<5>();
|
||||
bool result2 = IsAPIContractVxAvailable<5>();
|
||||
bool result3 = IsAPIContractVxAvailable<5>();
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsAPIContractVxAvailable_V10_ReturnsBoolean)
|
||||
{
|
||||
bool result = IsAPIContractVxAvailable<10>();
|
||||
// Result depends on Windows version, but should not crash
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsAPIContractVxAvailable_V15_ReturnsBoolean)
|
||||
{
|
||||
bool result = IsAPIContractVxAvailable<15>();
|
||||
// Higher API versions, may or may not be available
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
// Is19H1OrHigher tests
|
||||
TEST_METHOD(Is19H1OrHigher_ReturnsBoolean)
|
||||
{
|
||||
bool result = Is19H1OrHigher();
|
||||
// Result depends on OS version, but should not crash
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(Is19H1OrHigher_ReturnsSameAsV8Contract)
|
||||
{
|
||||
// Is19H1OrHigher is implemented as IsAPIContractV8Available
|
||||
bool is19H1 = Is19H1OrHigher();
|
||||
bool isV8 = IsAPIContractV8Available();
|
||||
Assert::AreEqual(is19H1, isV8);
|
||||
}
|
||||
|
||||
TEST_METHOD(Is19H1OrHigher_ConsistentAcrossMultipleCalls)
|
||||
{
|
||||
bool result1 = Is19H1OrHigher();
|
||||
bool result2 = Is19H1OrHigher();
|
||||
bool result3 = Is19H1OrHigher();
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
// Static caching behavior tests
|
||||
TEST_METHOD(StaticCaching_DifferentContractVersions_IndependentResults)
|
||||
{
|
||||
// Each template instantiation has its own static variable
|
||||
bool v1 = IsAPIContractVxAvailable<1>();
|
||||
(void)v1; // Suppress unused variable warning
|
||||
|
||||
// v1 should be true on any modern Windows
|
||||
Assert::IsTrue(v1);
|
||||
}
|
||||
|
||||
// Performance test (optional - verifies caching)
|
||||
TEST_METHOD(Performance_MultipleCallsAreFast)
|
||||
{
|
||||
// The static caching should make subsequent calls very fast
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 10000; ++i)
|
||||
{
|
||||
Is19H1OrHigher();
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// 10000 calls should complete in well under 1 second due to caching
|
||||
Assert::IsTrue(duration.count() < 1000);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <package.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace package;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(PackageTests)
|
||||
{
|
||||
public:
|
||||
// IsWin11OrGreater tests
|
||||
TEST_METHOD(IsWin11OrGreater_ReturnsBoolean)
|
||||
{
|
||||
bool result = IsWin11OrGreater();
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsWin11OrGreater_ConsistentResults)
|
||||
{
|
||||
bool result1 = IsWin11OrGreater();
|
||||
bool result2 = IsWin11OrGreater();
|
||||
bool result3 = IsWin11OrGreater();
|
||||
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
// PACKAGE_VERSION struct tests
|
||||
TEST_METHOD(PackageVersion_DefaultConstruction)
|
||||
{
|
||||
PACKAGE_VERSION version{};
|
||||
Assert::AreEqual(static_cast<UINT16>(0), version.Major);
|
||||
Assert::AreEqual(static_cast<UINT16>(0), version.Minor);
|
||||
Assert::AreEqual(static_cast<UINT16>(0), version.Build);
|
||||
Assert::AreEqual(static_cast<UINT16>(0), version.Revision);
|
||||
}
|
||||
|
||||
TEST_METHOD(PackageVersion_Assignment)
|
||||
{
|
||||
PACKAGE_VERSION version{};
|
||||
version.Major = 1;
|
||||
version.Minor = 2;
|
||||
version.Build = 3;
|
||||
version.Revision = 4;
|
||||
|
||||
Assert::AreEqual(static_cast<UINT16>(1), version.Major);
|
||||
Assert::AreEqual(static_cast<UINT16>(2), version.Minor);
|
||||
Assert::AreEqual(static_cast<UINT16>(3), version.Build);
|
||||
Assert::AreEqual(static_cast<UINT16>(4), version.Revision);
|
||||
}
|
||||
|
||||
// ComInitializer tests
|
||||
TEST_METHOD(ComInitializer_InitializesAndUninitializesCom)
|
||||
{
|
||||
{
|
||||
ComInitializer comInit;
|
||||
// COM should be initialized within this scope
|
||||
}
|
||||
// COM should be uninitialized after scope
|
||||
|
||||
// Verify we can initialize again
|
||||
{
|
||||
ComInitializer comInit2;
|
||||
}
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComInitializer_MultipleInstances)
|
||||
{
|
||||
ComInitializer init1;
|
||||
ComInitializer init2;
|
||||
ComInitializer init3;
|
||||
|
||||
// Multiple initializations should work (COM uses reference counting)
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// GetRegisteredPackage tests
|
||||
TEST_METHOD(GetRegisteredPackage_NonExistentPackage_ReturnsEmpty)
|
||||
{
|
||||
auto result = GetRegisteredPackage(L"NonExistentPackage12345", false);
|
||||
|
||||
// Should return empty for non-existent package
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetRegisteredPackage_EmptyName_DoesNotCrash)
|
||||
{
|
||||
auto result = GetRegisteredPackage(L"", false);
|
||||
// Behavior may vary based on package enumeration; just ensure it doesn't crash.
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// IsPackageRegisteredWithPowerToysVersion tests
|
||||
TEST_METHOD(IsPackageRegisteredWithPowerToysVersion_NonExistentPackage_ReturnsFalse)
|
||||
{
|
||||
bool result = IsPackageRegisteredWithPowerToysVersion(L"NonExistentPackage12345");
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsPackageRegisteredWithPowerToysVersion_EmptyName_ReturnsFalse)
|
||||
{
|
||||
bool result = IsPackageRegisteredWithPowerToysVersion(L"");
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
// FindMsixFile tests
|
||||
TEST_METHOD(FindMsixFile_NonExistentDirectory_ReturnsEmpty)
|
||||
{
|
||||
auto result = FindMsixFile(L"C:\\NonExistentDirectory12345", false);
|
||||
Assert::IsTrue(result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(FindMsixFile_SystemDirectory_DoesNotCrash)
|
||||
{
|
||||
// System32 probably doesn't have MSIX files, but shouldn't crash
|
||||
auto result = FindMsixFile(L"C:\\Windows\\System32", false);
|
||||
// May or may not find files, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(FindMsixFile_RecursiveSearch_DoesNotCrash)
|
||||
{
|
||||
// Use temp directory which should exist
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
auto result = FindMsixFile(tempPath, true);
|
||||
// May or may not find files, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// GetPackageNameAndVersionFromAppx tests
|
||||
TEST_METHOD(GetPackageNameAndVersionFromAppx_NonExistentFile_ReturnsFalse)
|
||||
{
|
||||
std::wstring name;
|
||||
PACKAGE_VERSION version{};
|
||||
|
||||
bool result = GetPackageNameAndVersionFromAppx(L"C:\\NonExistent\\file.msix", name, version);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetPackageNameAndVersionFromAppx_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
std::wstring name;
|
||||
PACKAGE_VERSION version{};
|
||||
|
||||
bool result = GetPackageNameAndVersionFromAppx(L"", name, version);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
// Thread safety
|
||||
TEST_METHOD(IsWin11OrGreater_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
IsWin11OrGreater();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <processApi.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ProcessApiTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(GetProcessHandlesByName_CurrentProcess_ReturnsHandles)
|
||||
{
|
||||
// Get current process executable name
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
|
||||
// Extract just the filename
|
||||
std::wstring fullPath(path);
|
||||
auto lastSlash = fullPath.rfind(L'\\');
|
||||
std::wstring exeName = (lastSlash != std::wstring::npos) ?
|
||||
fullPath.substr(lastSlash + 1) : fullPath;
|
||||
|
||||
auto handles = getProcessHandlesByName(exeName, PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
// Should find at least our own process
|
||||
Assert::IsFalse(handles.empty());
|
||||
|
||||
// Handles are RAII-managed
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_NonExistentProcess_ReturnsEmpty)
|
||||
{
|
||||
auto handles = getProcessHandlesByName(L"NonExistentProcess12345.exe", PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
Assert::IsTrue(handles.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_EmptyName_ReturnsEmpty)
|
||||
{
|
||||
auto handles = getProcessHandlesByName(L"", PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
Assert::IsTrue(handles.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_Explorer_ReturnsHandles)
|
||||
{
|
||||
// Explorer.exe should typically be running
|
||||
auto handles = getProcessHandlesByName(L"explorer.exe", PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
// Handles are RAII-managed
|
||||
|
||||
// May or may not find explorer depending on system state
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_CaseInsensitive_Works)
|
||||
{
|
||||
// Get current process name in uppercase
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
|
||||
std::wstring fullPath(path);
|
||||
auto lastSlash = fullPath.rfind(L'\\');
|
||||
std::wstring exeName = (lastSlash != std::wstring::npos) ?
|
||||
fullPath.substr(lastSlash + 1) : fullPath;
|
||||
|
||||
// Convert to uppercase
|
||||
std::wstring upperName = exeName;
|
||||
std::transform(upperName.begin(), upperName.end(), upperName.begin(), ::towupper);
|
||||
|
||||
auto handles = getProcessHandlesByName(upperName, PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
// Handles are RAII-managed
|
||||
|
||||
// The function may or may not be case insensitive - just don't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_DifferentAccessRights_Works)
|
||||
{
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
|
||||
std::wstring fullPath(path);
|
||||
auto lastSlash = fullPath.rfind(L'\\');
|
||||
std::wstring exeName = (lastSlash != std::wstring::npos) ?
|
||||
fullPath.substr(lastSlash + 1) : fullPath;
|
||||
|
||||
// Try with different access rights
|
||||
auto handles1 = getProcessHandlesByName(exeName, PROCESS_QUERY_INFORMATION);
|
||||
auto handles2 = getProcessHandlesByName(exeName, PROCESS_VM_READ);
|
||||
|
||||
// Handles are RAII-managed
|
||||
|
||||
// Just verify no crashes
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_SystemProcess_MayRequireElevation)
|
||||
{
|
||||
// System processes might require elevation
|
||||
auto handles = getProcessHandlesByName(L"System", PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
// Handles are RAII-managed
|
||||
|
||||
// Just verify no crashes
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_ValidHandles_AreUsable)
|
||||
{
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
|
||||
std::wstring fullPath(path);
|
||||
auto lastSlash = fullPath.rfind(L'\\');
|
||||
std::wstring exeName = (lastSlash != std::wstring::npos) ?
|
||||
fullPath.substr(lastSlash + 1) : fullPath;
|
||||
|
||||
auto handles = getProcessHandlesByName(exeName, PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
bool foundValidHandle = false;
|
||||
for (auto& handle : handles)
|
||||
{
|
||||
// Try to use the handle
|
||||
DWORD exitCode;
|
||||
if (GetExitCodeProcess(handle.get(), &exitCode))
|
||||
{
|
||||
foundValidHandle = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assert::IsTrue(foundValidHandle || handles.empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <process_path.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ProcessPathTests)
|
||||
{
|
||||
public:
|
||||
// get_process_path (by PID) tests
|
||||
TEST_METHOD(GetProcessPath_CurrentProcess_ReturnsPath)
|
||||
{
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
auto path = get_process_path(pid);
|
||||
|
||||
Assert::IsFalse(path.empty());
|
||||
Assert::IsTrue(path.find(L".exe") != std::wstring::npos ||
|
||||
path.find(L".dll") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_InvalidPid_ReturnsEmpty)
|
||||
{
|
||||
DWORD invalidPid = 0xFFFFFFFF;
|
||||
auto path = get_process_path(invalidPid);
|
||||
|
||||
// Should return empty for invalid PID
|
||||
Assert::IsTrue(path.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_ZeroPid_ReturnsEmpty)
|
||||
{
|
||||
auto path = get_process_path(static_cast<DWORD>(0));
|
||||
// PID 0 is the System Idle Process, might return empty or a path
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_SystemPid_DoesNotCrash)
|
||||
{
|
||||
// PID 4 is typically the System process
|
||||
auto path = get_process_path(static_cast<DWORD>(4));
|
||||
// May return empty due to access rights, but shouldn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// get_module_filename tests
|
||||
TEST_METHOD(GetModuleFilename_NullModule_ReturnsExePath)
|
||||
{
|
||||
auto path = get_module_filename(nullptr);
|
||||
|
||||
Assert::IsFalse(path.empty());
|
||||
Assert::IsTrue(path.find(L".exe") != std::wstring::npos ||
|
||||
path.find(L".dll") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetModuleFilename_Kernel32_ReturnsPath)
|
||||
{
|
||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
Assert::IsNotNull(kernel32);
|
||||
|
||||
auto path = get_module_filename(kernel32);
|
||||
|
||||
Assert::IsFalse(path.empty());
|
||||
// Should contain kernel32 (case insensitive check)
|
||||
std::wstring lowerPath = path;
|
||||
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), ::towlower);
|
||||
Assert::IsTrue(lowerPath.find(L"kernel32") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetModuleFilename_InvalidModule_ReturnsEmpty)
|
||||
{
|
||||
auto path = get_module_filename(reinterpret_cast<HMODULE>(0x12345678));
|
||||
// Invalid module should return empty
|
||||
Assert::IsTrue(path.empty());
|
||||
}
|
||||
|
||||
// get_module_folderpath tests
|
||||
TEST_METHOD(GetModuleFolderpath_NullModule_ReturnsFolder)
|
||||
{
|
||||
auto folder = get_module_folderpath(nullptr, true);
|
||||
|
||||
Assert::IsFalse(folder.empty());
|
||||
// Should not end with .exe when removeFilename is true
|
||||
Assert::IsTrue(folder.find(L".exe") == std::wstring::npos);
|
||||
// Should end with backslash or be a valid folder path
|
||||
Assert::IsTrue(folder.back() == L'\\' || folder.find(L"\\") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetModuleFolderpath_KeepFilename_ReturnsFullPath)
|
||||
{
|
||||
auto fullPath = get_module_folderpath(nullptr, false);
|
||||
|
||||
Assert::IsFalse(fullPath.empty());
|
||||
// Should contain .exe or .dll when not removing filename
|
||||
Assert::IsTrue(fullPath.find(L".exe") != std::wstring::npos ||
|
||||
fullPath.find(L".dll") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetModuleFolderpath_Kernel32_ReturnsSystem32)
|
||||
{
|
||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
Assert::IsNotNull(kernel32);
|
||||
|
||||
auto folder = get_module_folderpath(kernel32, true);
|
||||
|
||||
Assert::IsFalse(folder.empty());
|
||||
// Should be in system32 folder
|
||||
std::wstring lowerPath = folder;
|
||||
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), ::towlower);
|
||||
Assert::IsTrue(lowerPath.find(L"system32") != std::wstring::npos ||
|
||||
lowerPath.find(L"syswow64") != std::wstring::npos);
|
||||
}
|
||||
|
||||
// get_process_path (by HWND) tests
|
||||
TEST_METHOD(GetProcessPath_DesktopWindow_ReturnsPath)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
Assert::IsNotNull(desktop);
|
||||
|
||||
auto path = get_process_path(desktop);
|
||||
// Desktop window should return a path
|
||||
// (could be explorer.exe or empty depending on system)
|
||||
Assert::IsTrue(true); // Just verify it doesn't crash
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_InvalidHwnd_ReturnsEmpty)
|
||||
{
|
||||
auto path = get_process_path(reinterpret_cast<HWND>(0x12345678));
|
||||
Assert::IsTrue(path.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_NullHwnd_ReturnsEmpty)
|
||||
{
|
||||
auto path = get_process_path(static_cast<HWND>(nullptr));
|
||||
Assert::IsTrue(path.empty());
|
||||
}
|
||||
|
||||
// Consistency tests
|
||||
TEST_METHOD(Consistency_ModuleFilenameAndFolderpath_AreRelated)
|
||||
{
|
||||
auto fullPath = get_module_filename(nullptr);
|
||||
auto folder = get_module_folderpath(nullptr, true);
|
||||
|
||||
Assert::IsFalse(fullPath.empty());
|
||||
Assert::IsFalse(folder.empty());
|
||||
|
||||
// Full path should start with the folder
|
||||
Assert::IsTrue(fullPath.find(folder) == 0 || folder.find(fullPath.substr(0, folder.length())) == 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <ProcessWaiter.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace ProcessWaiter;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ProcessWaiterTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(OnProcessTerminate_InvalidPid_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
// Use a very unlikely PID (negative value as string will fail conversion)
|
||||
OnProcessTerminate(L"invalid", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
// Wait briefly
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// Should not crash, callback may or may not be called depending on implementation
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_NonExistentPid_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
// Use a PID that likely doesn't exist
|
||||
OnProcessTerminate(L"999999999", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
// Wait briefly
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_ZeroPid_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
OnProcessTerminate(L"0", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_CurrentProcessPid_DoesNotTerminate)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
// Use current process PID - it shouldn't terminate during test
|
||||
std::wstring pid = std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
OnProcessTerminate(pid, [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
// Wait briefly - current process should not terminate
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
// Callback should not have been called since process is still running
|
||||
Assert::IsFalse(called);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_EmptyCallback_DoesNotCrash)
|
||||
{
|
||||
// Test with an empty function
|
||||
OnProcessTerminate(L"999999999", std::function<void(DWORD)>());
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_MultipleCallsForSamePid_DoesNotCrash)
|
||||
{
|
||||
std::atomic<int> counter{ 0 };
|
||||
std::wstring pid = std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
// Multiple waits on same (running) process
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
OnProcessTerminate(pid, [&counter](DWORD) {
|
||||
counter++;
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
// None should have been called since process is running
|
||||
Assert::AreEqual(0, counter.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_NegativeNumberString_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
OnProcessTerminate(L"-1", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_LargeNumber_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
OnProcessTerminate(L"18446744073709551615", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <registry.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(RegistryTests)
|
||||
{
|
||||
public:
|
||||
// Note: These tests use HKCU which doesn't require elevation
|
||||
|
||||
TEST_METHOD(InstallScope_Registry_CanReadAndWrite)
|
||||
{
|
||||
TestHelpers::TestRegistryKey testKey(L"RegistryTest");
|
||||
Assert::IsTrue(testKey.isValid());
|
||||
|
||||
// Write a test value
|
||||
Assert::IsTrue(testKey.setStringValue(L"TestValue", L"TestData"));
|
||||
Assert::IsTrue(testKey.setDwordValue(L"TestDword", 42));
|
||||
}
|
||||
|
||||
TEST_METHOD(Registry_ValueChange_StringValue)
|
||||
{
|
||||
registry::ValueChange change{ HKEY_CURRENT_USER, L"Software\\PowerToys\\Test", L"TestValue", std::wstring{ L"TestData" } };
|
||||
|
||||
Assert::AreEqual(std::wstring(L"Software\\PowerToys\\Test"), change.path);
|
||||
Assert::IsTrue(change.name.has_value());
|
||||
Assert::AreEqual(std::wstring(L"TestValue"), *change.name);
|
||||
Assert::AreEqual(std::wstring(L"TestData"), std::get<std::wstring>(change.value));
|
||||
}
|
||||
|
||||
TEST_METHOD(Registry_ValueChange_DwordValue)
|
||||
{
|
||||
registry::ValueChange change{ HKEY_CURRENT_USER, L"Software\\PowerToys\\Test", L"TestDword", static_cast<DWORD>(42) };
|
||||
|
||||
Assert::AreEqual(std::wstring(L"Software\\PowerToys\\Test"), change.path);
|
||||
Assert::IsTrue(change.name.has_value());
|
||||
Assert::AreEqual(std::wstring(L"TestDword"), *change.name);
|
||||
Assert::AreEqual(static_cast<DWORD>(42), std::get<DWORD>(change.value));
|
||||
}
|
||||
|
||||
TEST_METHOD(Registry_ChangeSet_AddChanges)
|
||||
{
|
||||
registry::ChangeSet changeSet;
|
||||
|
||||
changeSet.changes.push_back({ HKEY_CURRENT_USER, L"Software\\PowerToys\\Test", L"Value1", std::wstring{ L"Data1" } });
|
||||
changeSet.changes.push_back({ HKEY_CURRENT_USER, L"Software\\PowerToys\\Test", L"Value2", static_cast<DWORD>(123) });
|
||||
|
||||
Assert::AreEqual(static_cast<size_t>(2), changeSet.changes.size());
|
||||
}
|
||||
|
||||
TEST_METHOD(InstallScope_GetCurrentInstallScope_ReturnsValidValue)
|
||||
{
|
||||
auto scope = registry::install_scope::get_current_install_scope();
|
||||
Assert::IsTrue(scope == registry::install_scope::InstallScope::PerMachine ||
|
||||
scope == registry::install_scope::InstallScope::PerUser);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <resources.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ResourcesTests)
|
||||
{
|
||||
public:
|
||||
// get_resource_string tests with current module
|
||||
TEST_METHOD(GetResourceString_NonExistentId_ReturnsFallback)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto result = get_resource_string(99999, instance, L"fallback");
|
||||
Assert::AreEqual(std::wstring(L"fallback"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetResourceString_NullInstance_UsesFallback)
|
||||
{
|
||||
auto result = get_resource_string(99999, nullptr, L"fallback");
|
||||
// Should return fallback or empty string
|
||||
Assert::IsTrue(result == L"fallback" || result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetResourceString_EmptyFallback_ReturnsEmpty)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto result = get_resource_string(99999, instance, L"");
|
||||
Assert::IsTrue(result.empty());
|
||||
}
|
||||
|
||||
// get_english_fallback_string tests
|
||||
TEST_METHOD(GetEnglishFallbackString_NonExistentId_ReturnsEmpty)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto result = get_english_fallback_string(99999, instance);
|
||||
// Should return empty or the resource if it exists
|
||||
Assert::IsTrue(true); // Just verify no crash
|
||||
}
|
||||
|
||||
TEST_METHOD(GetEnglishFallbackString_NullInstance_DoesNotCrash)
|
||||
{
|
||||
auto result = get_english_fallback_string(99999, nullptr);
|
||||
Assert::IsTrue(true); // Just verify no crash
|
||||
}
|
||||
|
||||
// get_resource_string_language_override tests
|
||||
TEST_METHOD(GetResourceStringLanguageOverride_NonExistentId_ReturnsEmpty)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto result = get_resource_string_language_override(99999, instance);
|
||||
// Should return empty for non-existent resource
|
||||
Assert::IsTrue(result.empty() || !result.empty()); // Valid either way
|
||||
}
|
||||
|
||||
TEST_METHOD(GetResourceStringLanguageOverride_NullInstance_DoesNotCrash)
|
||||
{
|
||||
auto result = get_resource_string_language_override(99999, nullptr);
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(GetResourceString_ThreadSafe)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount, instance]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
get_resource_string(99999, instance, L"fallback");
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// Kernel32 resource tests (has known resources)
|
||||
TEST_METHOD(GetResourceString_Kernel32_DoesNotCrash)
|
||||
{
|
||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
if (kernel32)
|
||||
{
|
||||
// Kernel32 has resources, but we don't know exact IDs
|
||||
// Just verify it doesn't crash
|
||||
get_resource_string(1, kernel32, L"fallback");
|
||||
get_resource_string(100, kernel32, L"fallback");
|
||||
get_resource_string(1000, kernel32, L"fallback");
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Performance test
|
||||
TEST_METHOD(GetResourceString_Performance_Acceptable)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
get_resource_string(99999, instance, L"fallback");
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// 1000 lookups should complete in under 1 second
|
||||
Assert::IsTrue(duration.count() < 1000);
|
||||
}
|
||||
|
||||
// Edge case tests
|
||||
TEST_METHOD(GetResourceString_ZeroId_DoesNotCrash)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
auto result = get_resource_string(0, instance, L"fallback");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetResourceString_MaxUintId_DoesNotCrash)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
auto result = get_resource_string(UINT_MAX, instance, L"fallback");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <serialized.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(SerializedTests)
|
||||
{
|
||||
public:
|
||||
// Basic Read tests
|
||||
TEST_METHOD(Read_DefaultState_ReturnsDefaultValue)
|
||||
{
|
||||
Serialized<int> s;
|
||||
int value = -1;
|
||||
s.Read([&value](const int& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(0, value); // Default constructed int is 0
|
||||
}
|
||||
|
||||
TEST_METHOD(Read_StringType_ReturnsEmpty)
|
||||
{
|
||||
Serialized<std::string> s;
|
||||
std::string value = "initial";
|
||||
s.Read([&value](const std::string& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(std::string(""), value);
|
||||
}
|
||||
|
||||
// Basic Access tests
|
||||
TEST_METHOD(Access_ModifyValue_ValueIsModified)
|
||||
{
|
||||
Serialized<int> s;
|
||||
s.Access([](int& v) {
|
||||
v = 42;
|
||||
});
|
||||
|
||||
int value = 0;
|
||||
s.Read([&value](const int& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(42, value);
|
||||
}
|
||||
|
||||
TEST_METHOD(Access_ModifyString_StringIsModified)
|
||||
{
|
||||
Serialized<std::string> s;
|
||||
s.Access([](std::string& v) {
|
||||
v = "hello";
|
||||
});
|
||||
|
||||
std::string value;
|
||||
s.Read([&value](const std::string& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(std::string("hello"), value);
|
||||
}
|
||||
|
||||
TEST_METHOD(Access_MultipleModifications_LastValuePersists)
|
||||
{
|
||||
Serialized<int> s;
|
||||
s.Access([](int& v) { v = 1; });
|
||||
s.Access([](int& v) { v = 2; });
|
||||
s.Access([](int& v) { v = 3; });
|
||||
|
||||
int value = 0;
|
||||
s.Read([&value](const int& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(3, value);
|
||||
}
|
||||
|
||||
// Reset tests
|
||||
TEST_METHOD(Reset_AfterModification_ReturnsDefault)
|
||||
{
|
||||
Serialized<int> s;
|
||||
s.Access([](int& v) { v = 42; });
|
||||
s.Reset();
|
||||
|
||||
int value = -1;
|
||||
s.Read([&value](const int& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(0, value);
|
||||
}
|
||||
|
||||
TEST_METHOD(Reset_String_ReturnsEmpty)
|
||||
{
|
||||
Serialized<std::string> s;
|
||||
s.Access([](std::string& v) { v = "hello"; });
|
||||
s.Reset();
|
||||
|
||||
std::string value = "initial";
|
||||
s.Read([&value](const std::string& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(std::string(""), value);
|
||||
}
|
||||
|
||||
// Complex type tests
|
||||
TEST_METHOD(Serialized_VectorType_Works)
|
||||
{
|
||||
Serialized<std::vector<int>> s;
|
||||
s.Access([](std::vector<int>& v) {
|
||||
v.push_back(1);
|
||||
v.push_back(2);
|
||||
v.push_back(3);
|
||||
});
|
||||
|
||||
size_t size = 0;
|
||||
int sum = 0;
|
||||
s.Read([&size, &sum](const std::vector<int>& v) {
|
||||
size = v.size();
|
||||
for (int i : v) sum += i;
|
||||
});
|
||||
|
||||
Assert::AreEqual(static_cast<size_t>(3), size);
|
||||
Assert::AreEqual(6, sum);
|
||||
}
|
||||
|
||||
TEST_METHOD(Serialized_MapType_Works)
|
||||
{
|
||||
Serialized<std::map<std::string, int>> s;
|
||||
s.Access([](std::map<std::string, int>& v) {
|
||||
v["one"] = 1;
|
||||
v["two"] = 2;
|
||||
});
|
||||
|
||||
int value = 0;
|
||||
s.Read([&value](const std::map<std::string, int>& v) {
|
||||
auto it = v.find("two");
|
||||
if (it != v.end()) {
|
||||
value = it->second;
|
||||
}
|
||||
});
|
||||
|
||||
Assert::AreEqual(2, value);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(ThreadSafety_ConcurrentReads_NoDataRace)
|
||||
{
|
||||
Serialized<int> s;
|
||||
s.Access([](int& v) { v = 42; });
|
||||
|
||||
std::atomic<int> readCount{ 0 };
|
||||
std::vector<std::thread> threads;
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&s, &readCount]() {
|
||||
for (int j = 0; j < 100; ++j)
|
||||
{
|
||||
s.Read([&readCount](const int& v) {
|
||||
if (v == 42) {
|
||||
readCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(1000, readCount.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(ThreadSafety_ConcurrentAccessAndRead_NoDataRace)
|
||||
{
|
||||
Serialized<int> s;
|
||||
std::atomic<bool> done{ false };
|
||||
std::atomic<int> accessCount{ 0 };
|
||||
std::atomic<int> readersReady{ 0 };
|
||||
std::atomic<bool> start{ false };
|
||||
|
||||
// Writer thread
|
||||
std::thread writer([&s, &done, &accessCount, &readersReady, &start]() {
|
||||
while (readersReady.load() < 5)
|
||||
{
|
||||
std::this_thread::yield();
|
||||
}
|
||||
start = true;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
s.Access([i](int& v) {
|
||||
v = i;
|
||||
});
|
||||
accessCount++;
|
||||
}
|
||||
done = true;
|
||||
});
|
||||
|
||||
// Reader threads
|
||||
std::vector<std::thread> readers;
|
||||
std::atomic<int> readAttempts{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
readers.emplace_back([&s, &done, &readAttempts, &readersReady, &start]() {
|
||||
readersReady++;
|
||||
while (!start)
|
||||
{
|
||||
std::this_thread::yield();
|
||||
}
|
||||
while (!done)
|
||||
{
|
||||
s.Read([](const int& v) {
|
||||
// Just read the value
|
||||
(void)v;
|
||||
});
|
||||
readAttempts++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
writer.join();
|
||||
for (auto& t : readers)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
// Verify all access calls completed
|
||||
Assert::AreEqual(100, accessCount.load());
|
||||
// Verify reads happened
|
||||
Assert::IsTrue(readAttempts > 0);
|
||||
}
|
||||
|
||||
// Struct type test
|
||||
TEST_METHOD(Serialized_StructType_Works)
|
||||
{
|
||||
struct TestStruct
|
||||
{
|
||||
int x = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
Serialized<TestStruct> s;
|
||||
s.Access([](TestStruct& v) {
|
||||
v.x = 10;
|
||||
v.name = "test";
|
||||
});
|
||||
|
||||
int x = 0;
|
||||
std::string name;
|
||||
s.Read([&x, &name](const TestStruct& v) {
|
||||
x = v.x;
|
||||
name = v.name;
|
||||
});
|
||||
|
||||
Assert::AreEqual(10, x);
|
||||
Assert::AreEqual(std::string("test"), name);
|
||||
}
|
||||
|
||||
TEST_METHOD(Reset_StructType_ResetsToDefault)
|
||||
{
|
||||
struct TestStruct
|
||||
{
|
||||
int x = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
Serialized<TestStruct> s;
|
||||
s.Access([](TestStruct& v) {
|
||||
v.x = 10;
|
||||
v.name = "test";
|
||||
});
|
||||
s.Reset();
|
||||
|
||||
int x = -1;
|
||||
std::string name = "not empty";
|
||||
s.Read([&x, &name](const TestStruct& v) {
|
||||
x = v.x;
|
||||
name = v.name;
|
||||
});
|
||||
|
||||
Assert::AreEqual(0, x);
|
||||
Assert::AreEqual(std::string(""), name);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <string_utils.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(StringUtilsTests)
|
||||
{
|
||||
public:
|
||||
// left_trim tests
|
||||
TEST_METHOD(LeftTrim_EmptyString_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = "";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_NoWhitespace_ReturnsOriginal)
|
||||
{
|
||||
std::string_view input = "hello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_LeadingSpaces_TrimsSpaces)
|
||||
{
|
||||
std::string_view input = " hello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_LeadingTabs_TrimsTabs)
|
||||
{
|
||||
std::string_view input = "\t\thello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_LeadingNewlines_TrimsNewlines)
|
||||
{
|
||||
std::string_view input = "\r\n\nhello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_MixedWhitespace_TrimsAll)
|
||||
{
|
||||
std::string_view input = " \t\r\nhello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_TrailingWhitespace_PreservesTrailing)
|
||||
{
|
||||
std::string_view input = " hello ";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello "), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_OnlyWhitespace_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = " \t\r\n";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_CustomChars_TrimsSpecified)
|
||||
{
|
||||
std::string_view input = "xxxhello";
|
||||
auto result = left_trim(input, std::string_view("x"));
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_WideString_Works)
|
||||
{
|
||||
std::wstring_view input = L" hello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::wstring_view(L"hello"), result);
|
||||
}
|
||||
|
||||
// right_trim tests
|
||||
TEST_METHOD(RightTrim_EmptyString_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = "";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_NoWhitespace_ReturnsOriginal)
|
||||
{
|
||||
std::string_view input = "hello";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_TrailingSpaces_TrimsSpaces)
|
||||
{
|
||||
std::string_view input = "hello ";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_TrailingTabs_TrimsTabs)
|
||||
{
|
||||
std::string_view input = "hello\t\t";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_TrailingNewlines_TrimsNewlines)
|
||||
{
|
||||
std::string_view input = "hello\r\n\n";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_LeadingWhitespace_PreservesLeading)
|
||||
{
|
||||
std::string_view input = " hello ";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view(" hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_OnlyWhitespace_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = " \t\r\n";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_CustomChars_TrimsSpecified)
|
||||
{
|
||||
std::string_view input = "helloxxx";
|
||||
auto result = right_trim(input, std::string_view("x"));
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_WideString_Works)
|
||||
{
|
||||
std::wstring_view input = L"hello ";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::wstring_view(L"hello"), result);
|
||||
}
|
||||
|
||||
// trim tests
|
||||
TEST_METHOD(Trim_EmptyString_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = "";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_NoWhitespace_ReturnsOriginal)
|
||||
{
|
||||
std::string_view input = "hello";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_BothSides_TrimsBoth)
|
||||
{
|
||||
std::string_view input = " hello ";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_MixedWhitespace_TrimsAll)
|
||||
{
|
||||
std::string_view input = " \t\r\nhello \t\r\n";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_InternalWhitespace_Preserved)
|
||||
{
|
||||
std::string_view input = " hello world ";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view("hello world"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_OnlyWhitespace_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = " \t\r\n ";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_CustomChars_TrimsSpecified)
|
||||
{
|
||||
std::string_view input = "xxxhelloxxx";
|
||||
auto result = trim(input, std::string_view("x"));
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_WideString_Works)
|
||||
{
|
||||
std::wstring_view input = L" hello ";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::wstring_view(L"hello"), result);
|
||||
}
|
||||
|
||||
// replace_chars tests
|
||||
TEST_METHOD(ReplaceChars_EmptyString_NoChange)
|
||||
{
|
||||
std::string s = "";
|
||||
replace_chars(s, std::string_view("abc"), 'x');
|
||||
Assert::AreEqual(std::string(""), s);
|
||||
}
|
||||
|
||||
TEST_METHOD(ReplaceChars_NoMatchingChars_NoChange)
|
||||
{
|
||||
std::string s = "hello";
|
||||
replace_chars(s, std::string_view("xyz"), '_');
|
||||
Assert::AreEqual(std::string("hello"), s);
|
||||
}
|
||||
|
||||
TEST_METHOD(ReplaceChars_SingleChar_Replaces)
|
||||
{
|
||||
std::string s = "hello";
|
||||
replace_chars(s, std::string_view("l"), '_');
|
||||
Assert::AreEqual(std::string("he__o"), s);
|
||||
}
|
||||
|
||||
TEST_METHOD(ReplaceChars_MultipleChars_ReplacesAll)
|
||||
{
|
||||
std::string s = "hello world";
|
||||
replace_chars(s, std::string_view("lo"), '_');
|
||||
Assert::AreEqual(std::string("he___ w_r_d"), s);
|
||||
}
|
||||
|
||||
TEST_METHOD(ReplaceChars_WideString_Works)
|
||||
{
|
||||
std::wstring s = L"hello";
|
||||
replace_chars(s, std::wstring_view(L"l"), L'_');
|
||||
Assert::AreEqual(std::wstring(L"he__o"), s);
|
||||
}
|
||||
|
||||
// unwide tests
|
||||
TEST_METHOD(Unwide_EmptyString_ReturnsEmpty)
|
||||
{
|
||||
std::wstring input = L"";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_AsciiString_Converts)
|
||||
{
|
||||
std::wstring input = L"hello";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_WithNumbers_Converts)
|
||||
{
|
||||
std::wstring input = L"test123";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("test123"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_WithSpecialChars_Converts)
|
||||
{
|
||||
std::wstring input = L"test!@#$%";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("test!@#$%"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_MixedCase_PreservesCase)
|
||||
{
|
||||
std::wstring input = L"HeLLo WoRLd";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("HeLLo WoRLd"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_LongString_Works)
|
||||
{
|
||||
std::wstring input = L"This is a longer string with multiple words and punctuation!";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("This is a longer string with multiple words and punctuation!"), result);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "pch.h"
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
|
||||
namespace TestHelpers
|
||||
{
|
||||
// RAII helper for creating and cleaning up temporary files
|
||||
class TempFile
|
||||
{
|
||||
public:
|
||||
TempFile(const std::wstring& content = L"", const std::wstring& extension = L".txt")
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
// Generate a unique filename
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dis(10000, 99999);
|
||||
|
||||
m_path = std::wstring(tempPath) + L"test_" + std::to_wstring(dis(gen)) + extension;
|
||||
|
||||
if (!content.empty())
|
||||
{
|
||||
std::wofstream file(m_path);
|
||||
file << content;
|
||||
}
|
||||
}
|
||||
|
||||
~TempFile()
|
||||
{
|
||||
if (std::filesystem::exists(m_path))
|
||||
{
|
||||
std::filesystem::remove(m_path);
|
||||
}
|
||||
}
|
||||
|
||||
TempFile(const TempFile&) = delete;
|
||||
TempFile& operator=(const TempFile&) = delete;
|
||||
|
||||
const std::wstring& path() const { return m_path; }
|
||||
|
||||
void write(const std::string& content)
|
||||
{
|
||||
std::ofstream file(m_path, std::ios::binary);
|
||||
file << content;
|
||||
}
|
||||
|
||||
void write(const std::wstring& content)
|
||||
{
|
||||
std::wofstream file(m_path);
|
||||
file << content;
|
||||
}
|
||||
|
||||
std::wstring read()
|
||||
{
|
||||
std::wifstream file(m_path);
|
||||
return std::wstring((std::istreambuf_iterator<wchar_t>(file)),
|
||||
std::istreambuf_iterator<wchar_t>());
|
||||
}
|
||||
|
||||
private:
|
||||
std::wstring m_path;
|
||||
};
|
||||
|
||||
// RAII helper for creating and cleaning up temporary directories
|
||||
class TempDirectory
|
||||
{
|
||||
public:
|
||||
TempDirectory()
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dis(10000, 99999);
|
||||
|
||||
m_path = std::wstring(tempPath) + L"testdir_" + std::to_wstring(dis(gen));
|
||||
std::filesystem::create_directories(m_path);
|
||||
}
|
||||
|
||||
~TempDirectory()
|
||||
{
|
||||
if (std::filesystem::exists(m_path))
|
||||
{
|
||||
std::filesystem::remove_all(m_path);
|
||||
}
|
||||
}
|
||||
|
||||
TempDirectory(const TempDirectory&) = delete;
|
||||
TempDirectory& operator=(const TempDirectory&) = delete;
|
||||
|
||||
const std::wstring& path() const { return m_path; }
|
||||
|
||||
private:
|
||||
std::wstring m_path;
|
||||
};
|
||||
|
||||
// Registry test key path - use HKCU for non-elevated tests
|
||||
inline const std::wstring TestRegistryPath = L"Software\\PowerToys\\UnitTests";
|
||||
|
||||
// RAII helper for registry key creation/cleanup
|
||||
class TestRegistryKey
|
||||
{
|
||||
public:
|
||||
TestRegistryKey(const std::wstring& subKey = L"")
|
||||
{
|
||||
m_path = TestRegistryPath;
|
||||
if (!subKey.empty())
|
||||
{
|
||||
m_path += L"\\" + subKey;
|
||||
}
|
||||
|
||||
HKEY key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, m_path.c_str(), 0, nullptr,
|
||||
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr,
|
||||
&key, nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(key);
|
||||
m_created = true;
|
||||
}
|
||||
}
|
||||
|
||||
~TestRegistryKey()
|
||||
{
|
||||
if (m_created)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, m_path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
TestRegistryKey(const TestRegistryKey&) = delete;
|
||||
TestRegistryKey& operator=(const TestRegistryKey&) = delete;
|
||||
|
||||
bool isValid() const { return m_created; }
|
||||
const std::wstring& path() const { return m_path; }
|
||||
|
||||
bool setStringValue(const std::wstring& name, const std::wstring& value)
|
||||
{
|
||||
HKEY key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, m_path.c_str(), 0, KEY_SET_VALUE, &key) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto result = RegSetValueExW(key, name.c_str(), 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE*>(value.c_str()),
|
||||
static_cast<DWORD>((value.length() + 1) * sizeof(wchar_t)));
|
||||
RegCloseKey(key);
|
||||
return result == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
bool setDwordValue(const std::wstring& name, DWORD value)
|
||||
{
|
||||
HKEY key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, m_path.c_str(), 0, KEY_SET_VALUE, &key) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto result = RegSetValueExW(key, name.c_str(), 0, REG_DWORD,
|
||||
reinterpret_cast<const BYTE*>(&value), sizeof(DWORD));
|
||||
RegCloseKey(key);
|
||||
return result == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
private:
|
||||
std::wstring m_path;
|
||||
bool m_created = false;
|
||||
};
|
||||
|
||||
// Helper to wait for a condition with timeout
|
||||
template<typename Predicate>
|
||||
bool WaitFor(Predicate pred, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000))
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!pred())
|
||||
{
|
||||
if (std::chrono::steady_clock::now() - start > timeout)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include <spdlog/sinks/null_sink.h>
|
||||
|
||||
std::shared_ptr<spdlog::logger> Logger::logger = spdlog::null_logger_mt("Common.Utils.UnitTests");
|
||||
|
||||
namespace PTSettingsHelper
|
||||
{
|
||||
std::wstring get_root_save_folder_location()
|
||||
{
|
||||
return L"";
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <OnThreadExecutor.h>
|
||||
#include <EventWaiter.h>
|
||||
#include <EventLocker.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(OnThreadExecutorTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(Constructor_CreatesInstance)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_SingleTask_Executes)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<bool> executed{ false };
|
||||
|
||||
auto future = executor.submit(OnThreadExecutor::task_t([&executed]() {
|
||||
executed = true;
|
||||
}));
|
||||
|
||||
future.wait();
|
||||
Assert::IsTrue(executed);
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_MultipleTasks_ExecutesAll)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<int> counter{ 0 };
|
||||
|
||||
std::vector<std::future<void>> futures;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
futures.push_back(executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
counter++;
|
||||
})));
|
||||
}
|
||||
|
||||
for (auto& f : futures)
|
||||
{
|
||||
f.wait();
|
||||
}
|
||||
|
||||
Assert::AreEqual(10, counter.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_TasksExecuteInOrder)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::vector<int> order;
|
||||
std::mutex orderMutex;
|
||||
|
||||
std::vector<std::future<void>> futures;
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
futures.push_back(executor.submit(OnThreadExecutor::task_t([&order, &orderMutex, i]() {
|
||||
std::lock_guard lock(orderMutex);
|
||||
order.push_back(i);
|
||||
})));
|
||||
}
|
||||
|
||||
for (auto& f : futures)
|
||||
{
|
||||
f.wait();
|
||||
}
|
||||
|
||||
Assert::AreEqual(static_cast<size_t>(5), order.size());
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
Assert::AreEqual(i, order[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_TaskReturnsResult)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<int> result{ 0 };
|
||||
|
||||
auto future = executor.submit(OnThreadExecutor::task_t([&result]() {
|
||||
result = 42;
|
||||
}));
|
||||
|
||||
future.wait();
|
||||
Assert::AreEqual(42, result.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(Cancel_ClearsPendingTasks)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<int> counter{ 0 };
|
||||
|
||||
// Submit a slow task first
|
||||
executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
counter++;
|
||||
}));
|
||||
|
||||
// Submit more tasks
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
counter++;
|
||||
}));
|
||||
}
|
||||
|
||||
// Cancel pending tasks
|
||||
executor.cancel();
|
||||
|
||||
// Wait a bit for any running task to complete
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
// Not all tasks should have executed
|
||||
Assert::IsTrue(counter < 6);
|
||||
}
|
||||
|
||||
TEST_METHOD(Destructor_WaitsForCompletion)
|
||||
{
|
||||
std::atomic<bool> completed{ false };
|
||||
std::future<void> future;
|
||||
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
future = executor.submit(OnThreadExecutor::task_t([&completed]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
completed = true;
|
||||
}));
|
||||
future.wait();
|
||||
} // Destructor no longer required to wait for completion
|
||||
|
||||
Assert::IsTrue(completed);
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_AfterCancel_StillWorks)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<int> counter{ 0 };
|
||||
|
||||
executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
counter++;
|
||||
}));
|
||||
executor.cancel();
|
||||
|
||||
auto future = executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
counter = 42;
|
||||
}));
|
||||
future.wait();
|
||||
|
||||
Assert::AreEqual(42, counter.load());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(EventWaiterTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(Constructor_CreatesInstance)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(Start_ValidEvent_ReturnsTrue)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
bool result = waiter.start(L"TestEvent_Start", [](DWORD) {});
|
||||
Assert::IsTrue(result);
|
||||
Assert::IsTrue(waiter.is_listening());
|
||||
waiter.stop();
|
||||
}
|
||||
|
||||
TEST_METHOD(Start_AlreadyListening_ReturnsFalse)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_Double1", [](DWORD) {});
|
||||
bool result = waiter.start(L"TestEvent_Double2", [](DWORD) {});
|
||||
Assert::IsFalse(result);
|
||||
waiter.stop();
|
||||
}
|
||||
|
||||
TEST_METHOD(Stop_WhileListening_StopsListening)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_Stop", [](DWORD) {});
|
||||
Assert::IsTrue(waiter.is_listening());
|
||||
|
||||
waiter.stop();
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(Stop_WhenNotListening_DoesNotCrash)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.stop(); // Should not crash
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(Stop_CalledMultipleTimes_DoesNotCrash)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_MultiStop", [](DWORD) {});
|
||||
waiter.stop();
|
||||
waiter.stop();
|
||||
waiter.stop();
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(Callback_EventSignaled_CallsCallback)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
std::atomic<bool> called{ false };
|
||||
std::atomic<DWORD> errorCode{ 0xFFFFFFFF };
|
||||
|
||||
// Create a named event we can signal
|
||||
std::wstring eventName = L"TestEvent_Callback_" + std::to_wstring(GetCurrentProcessId());
|
||||
HANDLE signalEvent = CreateEventW(nullptr, FALSE, FALSE, eventName.c_str());
|
||||
Assert::IsNotNull(signalEvent);
|
||||
|
||||
waiter.start(eventName, [&called, &errorCode](DWORD err) {
|
||||
errorCode = err;
|
||||
called = true;
|
||||
});
|
||||
|
||||
// Signal the event
|
||||
SetEvent(signalEvent);
|
||||
|
||||
// Wait for callback
|
||||
bool waitResult = TestHelpers::WaitFor([&called]() { return called.load(); }, std::chrono::milliseconds(1000));
|
||||
|
||||
waiter.stop();
|
||||
CloseHandle(signalEvent);
|
||||
|
||||
Assert::IsTrue(waitResult);
|
||||
Assert::AreEqual(static_cast<DWORD>(ERROR_SUCCESS), errorCode.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(Destructor_StopsListening)
|
||||
{
|
||||
std::atomic<bool> isListening{ false };
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_Destructor", [](DWORD) {});
|
||||
isListening = waiter.is_listening();
|
||||
}
|
||||
// After destruction, the waiter should have stopped
|
||||
Assert::IsTrue(isListening);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsListening_InitialState_ReturnsFalse)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(IsListening_AfterStart_ReturnsTrue)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_IsListening", [](DWORD) {});
|
||||
Assert::IsTrue(waiter.is_listening());
|
||||
waiter.stop();
|
||||
}
|
||||
|
||||
TEST_METHOD(IsListening_AfterStop_ReturnsFalse)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_AfterStop", [](DWORD) {});
|
||||
waiter.stop();
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(EventLockerTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(Get_ValidEventName_ReturnsLocker)
|
||||
{
|
||||
std::wstring eventName = L"TestEventLocker_" + std::to_wstring(GetCurrentProcessId());
|
||||
auto locker = EventLocker::Get(eventName);
|
||||
Assert::IsTrue(locker.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_UniqueNames_CreatesSeparateLockers)
|
||||
{
|
||||
auto locker1 = EventLocker::Get(L"TestEventLocker1_" + std::to_wstring(GetCurrentProcessId()));
|
||||
auto locker2 = EventLocker::Get(L"TestEventLocker2_" + std::to_wstring(GetCurrentProcessId()));
|
||||
Assert::IsTrue(locker1.has_value());
|
||||
Assert::IsTrue(locker2.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(Destructor_CleansUpHandle)
|
||||
{
|
||||
std::wstring eventName = L"TestEventLockerCleanup_" + std::to_wstring(GetCurrentProcessId());
|
||||
{
|
||||
auto locker = EventLocker::Get(eventName);
|
||||
Assert::IsTrue(locker.has_value());
|
||||
}
|
||||
// After destruction, the event should be cleaned up
|
||||
// Creating a new one should succeed
|
||||
auto newLocker = EventLocker::Get(eventName);
|
||||
Assert::IsTrue(newLocker.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(MoveConstructor_TransfersOwnership)
|
||||
{
|
||||
std::wstring eventName = L"TestEventLockerMove_" + std::to_wstring(GetCurrentProcessId());
|
||||
auto locker1 = EventLocker::Get(eventName);
|
||||
Assert::IsTrue(locker1.has_value());
|
||||
|
||||
EventLocker locker2 = std::move(*locker1);
|
||||
// Move should transfer ownership without crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(MoveAssignment_TransfersOwnership)
|
||||
{
|
||||
std::wstring eventName1 = L"TestEventLockerMoveAssign1_" + std::to_wstring(GetCurrentProcessId());
|
||||
std::wstring eventName2 = L"TestEventLockerMoveAssign2_" + std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
auto locker1 = EventLocker::Get(eventName1);
|
||||
auto locker2 = EventLocker::Get(eventName2);
|
||||
|
||||
Assert::IsTrue(locker1.has_value());
|
||||
Assert::IsTrue(locker2.has_value());
|
||||
|
||||
*locker1 = std::move(*locker2);
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <timeutil.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(TimeUtilsTests)
|
||||
{
|
||||
public:
|
||||
// to_string tests
|
||||
TEST_METHOD(ToString_ZeroTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 0;
|
||||
auto result = timeutil::to_string(t);
|
||||
Assert::AreEqual(std::wstring(L"0"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ToString_PositiveTime_ReturnsString)
|
||||
{
|
||||
time_t t = 1234567890;
|
||||
auto result = timeutil::to_string(t);
|
||||
Assert::AreEqual(std::wstring(L"1234567890"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ToString_LargeTime_ReturnsString)
|
||||
{
|
||||
time_t t = 1700000000;
|
||||
auto result = timeutil::to_string(t);
|
||||
Assert::AreEqual(std::wstring(L"1700000000"), result);
|
||||
}
|
||||
|
||||
// from_string tests
|
||||
TEST_METHOD(FromString_ZeroString_ReturnsZero)
|
||||
{
|
||||
auto result = timeutil::from_string(L"0");
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(static_cast<time_t>(0), result.value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_ValidNumber_ReturnsTime)
|
||||
{
|
||||
auto result = timeutil::from_string(L"1234567890");
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(static_cast<time_t>(1234567890), result.value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_InvalidString_ReturnsNullopt)
|
||||
{
|
||||
auto result = timeutil::from_string(L"invalid");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_EmptyString_ReturnsNullopt)
|
||||
{
|
||||
auto result = timeutil::from_string(L"");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_MixedAlphaNumeric_ReturnsNullopt)
|
||||
{
|
||||
auto result = timeutil::from_string(L"123abc");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_NegativeNumber_ReturnsNullopt)
|
||||
{
|
||||
auto result = timeutil::from_string(L"-1");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
// Roundtrip test
|
||||
TEST_METHOD(ToStringFromString_Roundtrip_Works)
|
||||
{
|
||||
time_t original = 1609459200; // 2021-01-01 00:00:00 UTC
|
||||
auto str = timeutil::to_string(original);
|
||||
auto result = timeutil::from_string(str);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(original, result.value());
|
||||
}
|
||||
|
||||
// now tests
|
||||
TEST_METHOD(Now_ReturnsReasonableTime)
|
||||
{
|
||||
auto result = timeutil::now();
|
||||
// Should be after 2020 and before 2100
|
||||
Assert::IsTrue(result > 1577836800); // 2020-01-01
|
||||
Assert::IsTrue(result < 4102444800); // 2100-01-01
|
||||
}
|
||||
|
||||
TEST_METHOD(Now_TwoCallsAreCloseInTime)
|
||||
{
|
||||
auto first = timeutil::now();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
auto second = timeutil::now();
|
||||
// Difference should be less than 2 seconds
|
||||
Assert::IsTrue(second >= first);
|
||||
Assert::IsTrue(second - first < 2);
|
||||
}
|
||||
|
||||
// diff::in_seconds tests
|
||||
TEST_METHOD(DiffInSeconds_SameTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 1000000;
|
||||
auto result = timeutil::diff::in_seconds(t, t);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInSeconds_OneDifference_ReturnsOne)
|
||||
{
|
||||
time_t to = 1000001;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_seconds(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(1), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInSeconds_60Seconds_Returns60)
|
||||
{
|
||||
time_t to = 1000060;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_seconds(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(60), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInSeconds_NegativeDiff_ReturnsNegative)
|
||||
{
|
||||
time_t to = 1000000;
|
||||
time_t from = 1000060;
|
||||
auto result = timeutil::diff::in_seconds(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(-60), result);
|
||||
}
|
||||
|
||||
// diff::in_minutes tests
|
||||
TEST_METHOD(DiffInMinutes_SameTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 1000000;
|
||||
auto result = timeutil::diff::in_minutes(t, t);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInMinutes_OneMinute_ReturnsOne)
|
||||
{
|
||||
time_t to = 1000060;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_minutes(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(1), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInMinutes_60Minutes_Returns60)
|
||||
{
|
||||
time_t to = 1003600;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_minutes(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(60), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInMinutes_LessThanMinute_ReturnsZero)
|
||||
{
|
||||
time_t to = 1000059;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_minutes(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
// diff::in_hours tests
|
||||
TEST_METHOD(DiffInHours_SameTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 1000000;
|
||||
auto result = timeutil::diff::in_hours(t, t);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInHours_OneHour_ReturnsOne)
|
||||
{
|
||||
time_t to = 1003600;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_hours(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(1), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInHours_24Hours_Returns24)
|
||||
{
|
||||
time_t to = 1086400;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_hours(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(24), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInHours_LessThanHour_ReturnsZero)
|
||||
{
|
||||
time_t to = 1003599;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_hours(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
// diff::in_days tests
|
||||
TEST_METHOD(DiffInDays_SameTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 1000000;
|
||||
auto result = timeutil::diff::in_days(t, t);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInDays_OneDay_ReturnsOne)
|
||||
{
|
||||
time_t to = 1086400;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_days(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(1), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInDays_7Days_Returns7)
|
||||
{
|
||||
time_t to = 1604800;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_days(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(7), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInDays_LessThanDay_ReturnsZero)
|
||||
{
|
||||
time_t to = 1086399;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_days(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
// format_as_local tests
|
||||
TEST_METHOD(FormatAsLocal_YearFormat_ReturnsYear)
|
||||
{
|
||||
time_t t = 1609459200; // 2021-01-01 00:00:00 UTC
|
||||
auto result = timeutil::format_as_local("%Y", t);
|
||||
// Result depends on local timezone, but year should be 2020 or 2021
|
||||
Assert::IsTrue(result == "2020" || result == "2021");
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAsLocal_DateFormat_ReturnsDate)
|
||||
{
|
||||
time_t t = 0; // 1970-01-01 00:00:00 UTC
|
||||
auto result = timeutil::format_as_local("%Y-%m-%d", t);
|
||||
// Result should be a date around 1970-01-01 depending on timezone
|
||||
Assert::IsTrue(result.length() == 10); // YYYY-MM-DD format
|
||||
Assert::IsTrue(result.substr(0, 4) == "1969" || result.substr(0, 4) == "1970");
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <UnhandledExceptionHandler.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(UnhandledExceptionTests)
|
||||
{
|
||||
public:
|
||||
// exceptionDescription tests
|
||||
TEST_METHOD(ExceptionDescription_AccessViolation_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_ACCESS_VIOLATION);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
// Should contain meaningful description
|
||||
std::string desc{ result };
|
||||
Assert::IsTrue(desc.find("ACCESS") != std::string::npos ||
|
||||
desc.find("access") != std::string::npos ||
|
||||
desc.find("violation") != std::string::npos ||
|
||||
desc.length() > 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_StackOverflow_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_STACK_OVERFLOW);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_DivideByZero_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_INT_DIVIDE_BY_ZERO);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_IllegalInstruction_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_ILLEGAL_INSTRUCTION);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_ArrayBoundsExceeded_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_Breakpoint_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_BREAKPOINT);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_SingleStep_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_SINGLE_STEP);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_FloatDivideByZero_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_FLT_DIVIDE_BY_ZERO);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_FloatOverflow_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_FLT_OVERFLOW);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_FloatUnderflow_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_FLT_UNDERFLOW);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_FloatInvalidOperation_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_FLT_INVALID_OPERATION);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_PrivilegedInstruction_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_PRIV_INSTRUCTION);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_InPageError_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_IN_PAGE_ERROR);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_UnknownCode_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(0x12345678);
|
||||
// Should return something (possibly "Unknown exception" or similar)
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_ZeroCode_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(0);
|
||||
// Should handle zero gracefully
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
// GetFilenameStart tests (if accessible)
|
||||
TEST_METHOD(GetFilenameStart_ValidPath_ReturnsFilename)
|
||||
{
|
||||
wchar_t path[] = L"C:\\folder\\subfolder\\file.exe";
|
||||
int start = GetFilenameStart(path);
|
||||
|
||||
Assert::IsTrue(start >= 0);
|
||||
Assert::AreEqual(std::wstring(L"file.exe"), std::wstring(path + start));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetFilenameStart_NoPath_ReturnsOriginal)
|
||||
{
|
||||
wchar_t path[] = L"file.exe";
|
||||
int start = GetFilenameStart(path);
|
||||
|
||||
Assert::IsTrue(start >= 0);
|
||||
Assert::AreEqual(std::wstring(L"file.exe"), std::wstring(path + start));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetFilenameStart_TrailingBackslash_ReturnsEmpty)
|
||||
{
|
||||
wchar_t path[] = L"C:\\folder\\";
|
||||
int start = GetFilenameStart(path);
|
||||
|
||||
// Should point to empty string after last backslash
|
||||
Assert::IsTrue(start >= 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetFilenameStart_NullPath_HandlesGracefully)
|
||||
{
|
||||
// This might crash or return null depending on implementation
|
||||
// Just document the behavior
|
||||
int start = GetFilenameStart(nullptr);
|
||||
(void)start;
|
||||
// Result is implementation-defined for null input
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(ExceptionDescription_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
auto desc = exceptionDescription(EXCEPTION_ACCESS_VIOLATION);
|
||||
if (desc && *desc != '\0')
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// All exception codes test
|
||||
TEST_METHOD(ExceptionDescription_AllCommonCodes_ReturnDescriptions)
|
||||
{
|
||||
std::vector<DWORD> codes = {
|
||||
EXCEPTION_ACCESS_VIOLATION,
|
||||
EXCEPTION_ARRAY_BOUNDS_EXCEEDED,
|
||||
EXCEPTION_BREAKPOINT,
|
||||
EXCEPTION_DATATYPE_MISALIGNMENT,
|
||||
EXCEPTION_FLT_DENORMAL_OPERAND,
|
||||
EXCEPTION_FLT_DIVIDE_BY_ZERO,
|
||||
EXCEPTION_FLT_INEXACT_RESULT,
|
||||
EXCEPTION_FLT_INVALID_OPERATION,
|
||||
EXCEPTION_FLT_OVERFLOW,
|
||||
EXCEPTION_FLT_STACK_CHECK,
|
||||
EXCEPTION_FLT_UNDERFLOW,
|
||||
EXCEPTION_ILLEGAL_INSTRUCTION,
|
||||
EXCEPTION_IN_PAGE_ERROR,
|
||||
EXCEPTION_INT_DIVIDE_BY_ZERO,
|
||||
EXCEPTION_INT_OVERFLOW,
|
||||
EXCEPTION_INVALID_DISPOSITION,
|
||||
EXCEPTION_NONCONTINUABLE_EXCEPTION,
|
||||
EXCEPTION_PRIV_INSTRUCTION,
|
||||
EXCEPTION_SINGLE_STEP,
|
||||
EXCEPTION_STACK_OVERFLOW
|
||||
};
|
||||
|
||||
for (DWORD code : codes)
|
||||
{
|
||||
auto desc = exceptionDescription(code);
|
||||
Assert::IsTrue(desc && *desc != '\0', (L"Empty description for code: " + std::to_wstring(code)).c_str());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#include <windows.h>
|
||||
#include "resource.h"
|
||||
#include "../version/version.h"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||
END
|
||||
END
|
||||
@@ -1,96 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{8B5CFB38-CCBA-40A8-AD7A-89C57B070884}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>UnitTestsCommonUtils</RootNamespace>
|
||||
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
|
||||
<ProjectName>Common.Utils.UnitTests</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UnitTestsCommonUtils\</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>..\;..\utils;..\Telemetry;..\..\;..\..\..\deps\;..\..\..\deps\spdlog\include;..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\include;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<LanguageStandard>stdcpp23</LanguageStandard>
|
||||
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_HEADER_ONLY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>RuntimeObject.lib;Msi.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="StringUtils.Tests.cpp" />
|
||||
<ClCompile Include="ColorUtils.Tests.cpp" />
|
||||
<ClCompile Include="TimeUtils.Tests.cpp" />
|
||||
<ClCompile Include="WinApiError.Tests.cpp" />
|
||||
<ClCompile Include="Serialized.Tests.cpp" />
|
||||
<ClCompile Include="Json.Tests.cpp" />
|
||||
<ClCompile Include="OsDetect.Tests.cpp" />
|
||||
<ClCompile Include="Threading.Tests.cpp" />
|
||||
<ClCompile Include="ProcessPath.Tests.cpp" />
|
||||
<ClCompile Include="Window.Tests.cpp" />
|
||||
<ClCompile Include="GameMode.Tests.cpp" />
|
||||
<ClCompile Include="Gpo.Tests.cpp" />
|
||||
<ClCompile Include="MsiUtils.Tests.cpp" />
|
||||
<ClCompile Include="HttpClient.Tests.cpp" />
|
||||
<ClCompile Include="ComObjectFactory.Tests.cpp" />
|
||||
<ClCompile Include="AppMutex.Tests.cpp" />
|
||||
<ClCompile Include="Elevation.Tests.cpp" />
|
||||
<ClCompile Include="Exec.Tests.cpp" />
|
||||
<ClCompile Include="ExcludedApps.Tests.cpp" />
|
||||
<ClCompile Include="HDropIterator.Tests.cpp" />
|
||||
<ClCompile Include="LoggerHelper.Tests.cpp" />
|
||||
<ClCompile Include="ModulesRegistry.Tests.cpp" />
|
||||
<ClCompile Include="MsWindowsSettings.Tests.cpp" />
|
||||
<ClCompile Include="Package.Tests.cpp" />
|
||||
<ClCompile Include="ProcessApi.Tests.cpp" />
|
||||
<ClCompile Include="ProcessWaiter.Tests.cpp" />
|
||||
<ClCompile Include="Registry.Tests.cpp" />
|
||||
<ClCompile Include="Resources.Tests.cpp" />
|
||||
<ClCompile Include="TestStubs.cpp" />
|
||||
<ClCompile Include="UnhandledException.Tests.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="TestHelpers.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="UnitTests-CommonUtils.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,143 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Pure Functions">
|
||||
<UniqueIdentifier>{A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Threading">
|
||||
<UniqueIdentifier>{B2C3D4E5-F6A7-4B6C-9D0E-1F2A3B4C5D6E}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Process">
|
||||
<UniqueIdentifier>{C3D4E5F6-A7B8-4C7D-0E1F-2A3B4C5D6E7F}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Registry">
|
||||
<UniqueIdentifier>{D4E5F6A7-B8C9-4D8E-1F2A-3B4C5D6E7F8A}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Integration">
|
||||
<UniqueIdentifier>{E5F6A7B8-C9D0-4E9F-2A3B-4C5D6E7F8A9B}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="StringUtils.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ColorUtils.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="TimeUtils.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="WinApiError.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Serialized.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Json.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ExcludedApps.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="OsDetect.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Threading.Tests.cpp">
|
||||
<Filter>Source Files\Threading</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="AppMutex.Tests.cpp">
|
||||
<Filter>Source Files\Threading</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ProcessWaiter.Tests.cpp">
|
||||
<Filter>Source Files\Threading</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ProcessPath.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ProcessApi.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Window.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Exec.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="GameMode.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="MsWindowsSettings.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Registry.Tests.cpp">
|
||||
<Filter>Source Files\Registry</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Gpo.Tests.cpp">
|
||||
<Filter>Source Files\Registry</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ModulesRegistry.Tests.cpp">
|
||||
<Filter>Source Files\Registry</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Elevation.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Package.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="MsiUtils.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HttpClient.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Resources.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LoggerHelper.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ComObjectFactory.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HDropIterator.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="UnhandledException.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="TestHelpers.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="UnitTests-CommonUtils.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,130 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <winapi_error.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(WinApiErrorTests)
|
||||
{
|
||||
public:
|
||||
// get_last_error_message tests
|
||||
TEST_METHOD(GetLastErrorMessage_Success_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_SUCCESS);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_FileNotFound_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_FILE_NOT_FOUND);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_AccessDenied_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_ACCESS_DENIED);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_PathNotFound_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_PATH_NOT_FOUND);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_InvalidHandle_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_INVALID_HANDLE);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_NotEnoughMemory_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_NOT_ENOUGH_MEMORY);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_InvalidParameter_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_INVALID_PARAMETER);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
// get_last_error_or_default tests
|
||||
TEST_METHOD(GetLastErrorOrDefault_Success_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_or_default(ERROR_SUCCESS);
|
||||
Assert::IsFalse(result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorOrDefault_FileNotFound_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_or_default(ERROR_FILE_NOT_FOUND);
|
||||
Assert::IsFalse(result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorOrDefault_AccessDenied_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_or_default(ERROR_ACCESS_DENIED);
|
||||
Assert::IsFalse(result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorOrDefault_UnknownError_ReturnsEmptyOrMessage)
|
||||
{
|
||||
// For an unknown error code, should return empty string or a default message
|
||||
auto result = get_last_error_or_default(0xFFFFFFFF);
|
||||
// Either empty or has content, both are valid
|
||||
Assert::IsTrue(result.empty() || !result.empty());
|
||||
}
|
||||
|
||||
// Comparison tests
|
||||
TEST_METHOD(BothFunctions_SameError_ProduceSameContent)
|
||||
{
|
||||
auto message = get_last_error_message(ERROR_FILE_NOT_FOUND);
|
||||
auto defaultMessage = get_last_error_or_default(ERROR_FILE_NOT_FOUND);
|
||||
|
||||
Assert::IsTrue(message.has_value());
|
||||
Assert::AreEqual(*message, defaultMessage);
|
||||
}
|
||||
|
||||
TEST_METHOD(BothFunctions_SuccessError_ProduceSameContent)
|
||||
{
|
||||
auto message = get_last_error_message(ERROR_SUCCESS);
|
||||
auto defaultMessage = get_last_error_or_default(ERROR_SUCCESS);
|
||||
|
||||
Assert::IsTrue(message.has_value());
|
||||
Assert::AreEqual(*message, defaultMessage);
|
||||
}
|
||||
|
||||
// Error code specific tests
|
||||
TEST_METHOD(GetLastErrorMessage_SharingViolation_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_SHARING_VIOLATION);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_FileExists_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_FILE_EXISTS);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_DirNotEmpty_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_DIR_NOT_EMPTY);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <window.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(WindowTests)
|
||||
{
|
||||
public:
|
||||
// is_system_window tests
|
||||
TEST_METHOD(IsSystemWindow_DesktopWindow_ReturnsResult)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
Assert::IsNotNull(desktop);
|
||||
|
||||
// Get class name
|
||||
char className[256] = {};
|
||||
GetClassNameA(desktop, className, sizeof(className));
|
||||
|
||||
bool result = is_system_window(desktop, className);
|
||||
// Just verify it doesn't crash and returns a boolean
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsSystemWindow_NullHwnd_ReturnsFalse)
|
||||
{
|
||||
auto shell = GetShellWindow();
|
||||
auto desktop = GetDesktopWindow();
|
||||
bool result = is_system_window(nullptr, "ClassName");
|
||||
bool expected = (shell == nullptr) || (desktop == nullptr);
|
||||
Assert::AreEqual(expected, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsSystemWindow_InvalidHwnd_ReturnsFalse)
|
||||
{
|
||||
bool result = is_system_window(reinterpret_cast<HWND>(0x12345678), "ClassName");
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsSystemWindow_EmptyClassName_DoesNotCrash)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
bool result = is_system_window(desktop, "");
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsSystemWindow_NullClassName_DoesNotCrash)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
bool result = is_system_window(desktop, nullptr);
|
||||
// Should handle null className gracefully
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
// GetWindowCreateParam tests
|
||||
TEST_METHOD(GetWindowCreateParam_ValidLparam_ReturnsValue)
|
||||
{
|
||||
struct TestData
|
||||
{
|
||||
int value;
|
||||
};
|
||||
|
||||
TestData data{ 42 };
|
||||
CREATESTRUCT cs{};
|
||||
cs.lpCreateParams = &data;
|
||||
|
||||
auto result = GetWindowCreateParam<TestData*>(reinterpret_cast<LPARAM>(&cs));
|
||||
Assert::IsNotNull(result);
|
||||
Assert::AreEqual(42, result->value);
|
||||
}
|
||||
|
||||
// Window data storage tests
|
||||
TEST_METHOD(WindowData_StoreAndRetrieve_Works)
|
||||
{
|
||||
// Create a simple message-only window for testing
|
||||
WNDCLASSW wc = {};
|
||||
wc.lpfnWndProc = DefWindowProcW;
|
||||
wc.hInstance = GetModuleHandleW(nullptr);
|
||||
wc.lpszClassName = L"TestWindowClass_DataTest";
|
||||
RegisterClassW(&wc);
|
||||
|
||||
HWND hwnd = CreateWindowExW(0, L"TestWindowClass_DataTest", L"Test",
|
||||
0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
|
||||
GetModuleHandleW(nullptr), nullptr);
|
||||
|
||||
if (hwnd)
|
||||
{
|
||||
int value = 42;
|
||||
int* testValue = &value;
|
||||
StoreWindowParam(hwnd, testValue);
|
||||
|
||||
auto retrieved = GetWindowParam<int*>(hwnd);
|
||||
Assert::AreEqual(testValue, retrieved);
|
||||
|
||||
DestroyWindow(hwnd);
|
||||
}
|
||||
|
||||
UnregisterClassW(L"TestWindowClass_DataTest", GetModuleHandleW(nullptr));
|
||||
Assert::IsTrue(true); // Window creation might fail in test environment
|
||||
}
|
||||
|
||||
// run_message_loop tests
|
||||
TEST_METHOD(RunMessageLoop_UntilIdle_Completes)
|
||||
{
|
||||
// Run message loop until idle with a timeout
|
||||
// This should complete quickly since there are no messages
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
run_message_loop(true, 100);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should complete within reasonable time
|
||||
Assert::IsTrue(elapsed.count() < 500);
|
||||
}
|
||||
|
||||
TEST_METHOD(RunMessageLoop_WithTimeout_RespectsTimeout)
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
run_message_loop(false, 50);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should take at least the timeout duration
|
||||
// Allow some tolerance for timing
|
||||
Assert::IsTrue(elapsed.count() >= 40 && elapsed.count() < 500);
|
||||
}
|
||||
|
||||
TEST_METHOD(RunMessageLoop_ZeroTimeout_CompletesImmediately)
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
run_message_loop(false, 0);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should complete very quickly
|
||||
Assert::IsTrue(elapsed.count() < 100);
|
||||
}
|
||||
|
||||
TEST_METHOD(RunMessageLoop_NoTimeout_ProcessesMessages)
|
||||
{
|
||||
// Post a quit message before starting the loop
|
||||
PostQuitMessage(0);
|
||||
|
||||
// Should process the quit message and exit
|
||||
run_message_loop(false, std::nullopt);
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -1,5 +0,0 @@
|
||||
// pch.cpp: source file corresponding to the pre-compiled header
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
|
||||
@@ -1,39 +0,0 @@
|
||||
// pch.h: This is a precompiled header file.
|
||||
// Files listed below are compiled only once, improving build performance for future builds.
|
||||
// This also affects IntelliSense performance, including code completion and many code browsing features.
|
||||
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
|
||||
// Do not add files here that you will be updating frequently as this negates the performance advantage.
|
||||
|
||||
#ifndef PCH_H
|
||||
#define PCH_H
|
||||
|
||||
// add headers that you want to pre-compile here
|
||||
#include <Windows.h>
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.Foundation.Metadata.h>
|
||||
#include <winrt/Windows.Data.Json.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <future>
|
||||
#include <queue>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
|
||||
// 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,13 +0,0 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by UnitTests-CommonUtils.rc
|
||||
|
||||
//////////////////////////////
|
||||
// Non-localizable
|
||||
|
||||
#define FILE_DESCRIPTION "PowerToys UnitTests-CommonUtils"
|
||||
#define INTERNAL_NAME "UnitTests-CommonUtils"
|
||||
#define ORIGINAL_FILENAME "UnitTests-CommonUtils.dll"
|
||||
|
||||
// Non-localizable
|
||||
//////////////////////////////
|
||||
@@ -251,40 +251,4 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
|
||||
}
|
||||
hstring Constants::TogglePowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::TerminatePowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::RefreshPowerDisplayMonitorsEvent()
|
||||
{
|
||||
return CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT;
|
||||
}
|
||||
hstring Constants::SettingsUpdatedPowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::PowerDisplaySendSettingsTelemetryEvent()
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT;
|
||||
}
|
||||
hstring Constants::HotkeyUpdatedPowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::HOTKEY_UPDATED_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::PowerDisplayToggleMessage()
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE;
|
||||
}
|
||||
hstring Constants::PowerDisplayApplyProfileMessage()
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_APPLY_PROFILE_MESSAGE;
|
||||
}
|
||||
hstring Constants::PowerDisplayTerminateAppMessage()
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,15 +66,6 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring WorkspacesHotkeyEvent();
|
||||
static hstring PowerToysRunnerTerminateSettingsEvent();
|
||||
static hstring ShowCmdPalEvent();
|
||||
static hstring TogglePowerDisplayEvent();
|
||||
static hstring TerminatePowerDisplayEvent();
|
||||
static hstring RefreshPowerDisplayMonitorsEvent();
|
||||
static hstring SettingsUpdatedPowerDisplayEvent();
|
||||
static hstring PowerDisplaySendSettingsTelemetryEvent();
|
||||
static hstring HotkeyUpdatedPowerDisplayEvent();
|
||||
static hstring PowerDisplayToggleMessage();
|
||||
static hstring PowerDisplayApplyProfileMessage();
|
||||
static hstring PowerDisplayTerminateAppMessage();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -63,15 +63,6 @@ namespace PowerToys
|
||||
static String WorkspacesHotkeyEvent();
|
||||
static String PowerToysRunnerTerminateSettingsEvent();
|
||||
static String ShowCmdPalEvent();
|
||||
static String TogglePowerDisplayEvent();
|
||||
static String TerminatePowerDisplayEvent();
|
||||
static String RefreshPowerDisplayMonitorsEvent();
|
||||
static String SettingsUpdatedPowerDisplayEvent();
|
||||
static String PowerDisplaySendSettingsTelemetryEvent();
|
||||
static String HotkeyUpdatedPowerDisplayEvent();
|
||||
static String PowerDisplayToggleMessage();
|
||||
static String PowerDisplayApplyProfileMessage();
|
||||
static String PowerDisplayTerminateAppMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,23 +153,6 @@ namespace CommonSharedConstants
|
||||
const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30";
|
||||
const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512";
|
||||
|
||||
// Path to the events used by PowerDisplay
|
||||
const wchar_t TOGGLE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c";
|
||||
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
|
||||
const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
|
||||
const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
|
||||
const wchar_t POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsTelemetryEvent-8c4f2a1d-5e3b-7f9c-1a6d-3b8e5f2c9a7d";
|
||||
const wchar_t HOTKEY_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-HotkeyUpdatedEvent-9d5f3a2b-7e1c-4b8a-6f3d-2a9e5c7b1d4f";
|
||||
|
||||
// IPC Messages used in PowerDisplay (Named Pipe communication)
|
||||
const wchar_t POWER_DISPLAY_TOGGLE_MESSAGE[] = L"Toggle";
|
||||
const wchar_t POWER_DISPLAY_APPLY_PROFILE_MESSAGE[] = L"ApplyProfile";
|
||||
const wchar_t POWER_DISPLAY_TERMINATE_APP_MESSAGE[] = L"TerminateApp";
|
||||
|
||||
// Path to the events used by LightSwitch to notify PowerDisplay of theme changes
|
||||
const wchar_t LIGHT_SWITCH_LIGHT_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca";
|
||||
const wchar_t LIGHT_SWITCH_DARK_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368";
|
||||
|
||||
// used from quick access window
|
||||
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
||||
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";
|
||||
|
||||
@@ -83,7 +83,6 @@ struct LogSettings
|
||||
inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.log";
|
||||
inline const static std::string zoomItLoggerName = "zoom-it";
|
||||
inline const static std::string lightSwitchLoggerName = "light-switch";
|
||||
inline const static std::string powerDisplayLoggerName = "powerdisplay";
|
||||
inline const static int retention = 30;
|
||||
std::wstring logLevel;
|
||||
LogSettings();
|
||||
|
||||
@@ -34,7 +34,6 @@ public:
|
||||
{
|
||||
this->eventHandle = e.eventHandle;
|
||||
e.eventHandle = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~EventLocker()
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include "../logger/logger.h"
|
||||
|
||||
inline bool GetAnimationsEnabled()
|
||||
{
|
||||
BOOL enabled = 0;
|
||||
@@ -13,4 +10,4 @@ inline bool GetAnimationsEnabled()
|
||||
Logger::error("SystemParametersInfo SPI_GETCLIENTAREAANIMATION failed.");
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,7 @@ namespace ProcessWaiter
|
||||
{
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
try
|
||||
{
|
||||
pid = std::stol(parent_pid);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
if (callback)
|
||||
{
|
||||
callback(ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
return;
|
||||
}
|
||||
DWORD pid = std::stol(parent_pid);
|
||||
std::thread([=]() {
|
||||
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
if (process != nullptr)
|
||||
@@ -27,26 +15,17 @@ namespace ProcessWaiter
|
||||
if (WaitForSingleObject(process, INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
CloseHandle(process);
|
||||
if (callback)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseHandle(process);
|
||||
if (callback)
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (callback)
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
callback(GetLastError());
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
@@ -31,10 +31,6 @@ public:
|
||||
|
||||
HRESULT __stdcall CreateInstance(IUnknown* punkOuter, const IID& riid, void** ppv)
|
||||
{
|
||||
if (!ppv)
|
||||
{
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppv = nullptr;
|
||||
|
||||
if (punkOuter)
|
||||
@@ -59,4 +55,4 @@ public:
|
||||
|
||||
private:
|
||||
std::atomic<long> _refCount;
|
||||
};
|
||||
};
|
||||
@@ -32,7 +32,6 @@ namespace powertoys_gpo
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH = L"ConfigureEnabledUtilityLightSwitch";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_DISPLAY = L"ConfigureEnabledUtilityPowerDisplay";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview";
|
||||
@@ -311,11 +310,6 @@ namespace powertoys_gpo
|
||||
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredPowerDisplayEnabledValue()
|
||||
{
|
||||
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_DISPLAY);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue()
|
||||
{
|
||||
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_FANCYZONES);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <filesystem>
|
||||
#include <common/version/version.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace LoggerHelpers
|
||||
{
|
||||
|
||||
@@ -21,16 +21,9 @@
|
||||
|
||||
namespace package
|
||||
{
|
||||
using winrt::Windows::ApplicationModel::Package;
|
||||
using winrt::Windows::Foundation::IAsyncOperationWithProgress;
|
||||
using winrt::Windows::Foundation::AsyncStatus;
|
||||
using winrt::Windows::Foundation::Uri;
|
||||
using winrt::Windows::Foundation::Collections::IVector;
|
||||
using winrt::Windows::Management::Deployment::AddPackageOptions;
|
||||
using winrt::Windows::Management::Deployment::DeploymentOptions;
|
||||
using winrt::Windows::Management::Deployment::DeploymentProgress;
|
||||
using winrt::Windows::Management::Deployment::DeploymentResult;
|
||||
using winrt::Windows::Management::Deployment::PackageManager;
|
||||
using namespace winrt::Windows::Foundation;
|
||||
using namespace winrt::Windows::ApplicationModel;
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
inline BOOL IsWin11OrGreater()
|
||||
@@ -442,7 +435,7 @@ namespace package
|
||||
// Declare use of an external location
|
||||
DeploymentOptions options = DeploymentOptions::ForceTargetApplicationShutdown;
|
||||
|
||||
IVector<Uri> uris = winrt::single_threaded_vector<Uri>();
|
||||
Collections::IVector<Uri> uris = winrt::single_threaded_vector<Uri>();
|
||||
if (!dependencies.empty())
|
||||
{
|
||||
for (const auto& dependency : dependencies)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <cinttypes>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <cwctype>
|
||||
|
||||
#include <winrt/base.h>
|
||||
|
||||
@@ -28,17 +27,6 @@ namespace timeutil
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
for (wchar_t ch : s)
|
||||
{
|
||||
if (!iswdigit(ch))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
uint64_t i = std::stoull(s);
|
||||
return static_cast<std::time_t>(i);
|
||||
}
|
||||
|
||||
@@ -149,16 +149,6 @@
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
|
||||
<enabledValue>
|
||||
<decimal value="1" />
|
||||
</enabledValue>
|
||||
<disabledValue>
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="ConfigureEnabledUtilityEnvironmentVariables" class="Both" displayName="$(string.ConfigureEnabledUtilityEnvironmentVariables)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityEnvironmentVariables">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />
|
||||
|
||||
@@ -248,7 +248,6 @@ If you don't configure this policy, the user will be able to control the setting
|
||||
<string id="ConfigureEnabledUtilityCmdPal">CmdPal: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityLightSwitch">Light Switch: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>
|
||||
|
||||
@@ -250,6 +250,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
|
||||
Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute);
|
||||
stateManager.SyncInitialThemeState();
|
||||
stateManager.OnTick(nowMinutes);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Worker Loop
|
||||
@@ -280,7 +281,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
GetLocalTime(&st);
|
||||
nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
DetectAndHandleExternalThemeChange(stateManager);
|
||||
stateManager.OnTick();
|
||||
stateManager.OnTick(nowMinutes);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -248,46 +248,6 @@ void LightSwitchSettings::LoadSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// EnableDarkModeProfile
|
||||
if (const auto jsonVal = values.get_bool_value(L"enableDarkModeProfile"))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.enableDarkModeProfile != val)
|
||||
{
|
||||
m_settings.enableDarkModeProfile = val;
|
||||
}
|
||||
}
|
||||
|
||||
// EnableLightModeProfile
|
||||
if (const auto jsonVal = values.get_bool_value(L"enableLightModeProfile"))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.enableLightModeProfile != val)
|
||||
{
|
||||
m_settings.enableLightModeProfile = val;
|
||||
}
|
||||
}
|
||||
|
||||
// DarkModeProfile
|
||||
if (const auto jsonVal = values.get_string_value(L"darkModeProfile"))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.darkModeProfile != val)
|
||||
{
|
||||
m_settings.darkModeProfile = val;
|
||||
}
|
||||
}
|
||||
|
||||
// LightModeProfile
|
||||
if (const auto jsonVal = values.get_string_value(L"lightModeProfile"))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.lightModeProfile != val)
|
||||
{
|
||||
m_settings.lightModeProfile = val;
|
||||
}
|
||||
}
|
||||
|
||||
// For ChangeSystem/ChangeApps changes, log telemetry
|
||||
if (themeTargetChanged)
|
||||
{
|
||||
|
||||
@@ -67,11 +67,6 @@ struct LightSwitchConfig
|
||||
|
||||
bool changeSystem = false;
|
||||
bool changeApps = false;
|
||||
|
||||
bool enableDarkModeProfile = false;
|
||||
bool enableLightModeProfile = false;
|
||||
std::wstring darkModeProfile = L"";
|
||||
std::wstring lightModeProfile = L"";
|
||||
};
|
||||
|
||||
class LightSwitchSettings
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <LightSwitchUtils.h>
|
||||
#include "ThemeScheduler.h"
|
||||
#include <ThemeHelper.h>
|
||||
#include <common/interop/shared_constants.h>
|
||||
|
||||
void ApplyTheme(bool shouldBeLight);
|
||||
|
||||
@@ -29,7 +28,7 @@ void LightSwitchStateManager::OnSettingsChanged()
|
||||
}
|
||||
|
||||
// Called once per minute
|
||||
void LightSwitchStateManager::OnTick()
|
||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
if (_state.lastAppliedMode != ScheduleMode::FollowNightLight)
|
||||
@@ -38,7 +37,7 @@ void LightSwitchStateManager::OnTick()
|
||||
}
|
||||
}
|
||||
|
||||
// Called when manual override is triggered (via hotkey)
|
||||
// Called when manual override is triggered
|
||||
void LightSwitchStateManager::OnManualOverride()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
@@ -46,19 +45,15 @@ void LightSwitchStateManager::OnManualOverride()
|
||||
_state.isManualOverride = !_state.isManualOverride;
|
||||
|
||||
// When entering manual override, sync internal theme state to match the current system
|
||||
// The hotkey handler in ModuleInterface has already toggled the theme, so we read the new state
|
||||
if (_state.isManualOverride)
|
||||
{
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
|
||||
// Notify PowerDisplay about the theme change triggered by hotkey
|
||||
// The theme has already been applied by ModuleInterface, we just need to notify PowerDisplay
|
||||
NotifyPowerDisplay(_state.isSystemLightActive);
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
@@ -114,14 +109,10 @@ void LightSwitchStateManager::SyncInitialThemeState()
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
_state.isNightLightActive = IsNightLightEnabled();
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
|
||||
// This will ensure that the theme is applied according to current settings at startup
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
static std::pair<int, int> update_sun_times(auto& settings)
|
||||
@@ -273,61 +264,7 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
// Notify PowerDisplay to apply display profile if configured
|
||||
NotifyPowerDisplay(shouldBeLight);
|
||||
}
|
||||
|
||||
_state.lastTickMinutes = now;
|
||||
}
|
||||
|
||||
// Notify PowerDisplay module about theme change to apply display profiles
|
||||
void LightSwitchStateManager::NotifyPowerDisplay(bool isLight)
|
||||
{
|
||||
const auto& settings = LightSwitchSettings::settings();
|
||||
|
||||
// Check if any profile is enabled and configured
|
||||
bool shouldNotify = false;
|
||||
|
||||
if (isLight && settings.enableLightModeProfile && !settings.lightModeProfile.empty())
|
||||
{
|
||||
shouldNotify = true;
|
||||
}
|
||||
else if (!isLight && settings.enableDarkModeProfile && !settings.darkModeProfile.empty())
|
||||
{
|
||||
shouldNotify = true;
|
||||
}
|
||||
|
||||
if (!shouldNotify)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Signal PowerDisplay with the specific theme event
|
||||
// Using separate events for light/dark eliminates race conditions where PowerDisplay
|
||||
// might read the registry before LightSwitch has finished updating it
|
||||
const wchar_t* eventName = isLight
|
||||
? CommonSharedConstants::LIGHT_SWITCH_LIGHT_THEME_EVENT
|
||||
: CommonSharedConstants::LIGHT_SWITCH_DARK_THEME_EVENT;
|
||||
|
||||
Logger::info(L"[LightSwitchStateManager] Notifying PowerDisplay about theme change (isLight: {})", isLight);
|
||||
|
||||
HANDLE hThemeEvent = CreateEventW(nullptr, FALSE, FALSE, eventName);
|
||||
if (hThemeEvent)
|
||||
{
|
||||
SetEvent(hThemeEvent);
|
||||
CloseHandle(hThemeEvent);
|
||||
Logger::info(L"[LightSwitchStateManager] Theme event signaled to PowerDisplay: {}", eventName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"[LightSwitchStateManager] Failed to create theme event (error: {})", GetLastError());
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"[LightSwitchStateManager] Failed to notify PowerDisplay");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public:
|
||||
void OnSettingsChanged();
|
||||
|
||||
// Called every minute (from service worker tick).
|
||||
void OnTick();
|
||||
void OnTick(int currentMinutes);
|
||||
|
||||
// Called when manual override is toggled (via shortcut or system change).
|
||||
void OnManualOverride();
|
||||
@@ -48,7 +48,4 @@ private:
|
||||
|
||||
void EvaluateAndApplyIfNeeded();
|
||||
bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon);
|
||||
|
||||
// Notify PowerDisplay module about theme change to apply display profiles
|
||||
void NotifyPowerDisplay(bool isLight);
|
||||
};
|
||||
|
||||
@@ -32,7 +32,6 @@ using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
@@ -73,8 +72,6 @@ public partial class App : Application, IDisposable
|
||||
|
||||
Services = ConfigureServices();
|
||||
|
||||
IconCacheProvider.Initialize(Services);
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
// Ensure types used in XAML are preserved for AOT compilation
|
||||
@@ -116,13 +113,12 @@ public partial class App : Application, IDisposable
|
||||
|
||||
// Root services
|
||||
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
AddBuiltInCommands(services);
|
||||
|
||||
AddCoreServices(services);
|
||||
|
||||
AddUIServices(services, dispatcherQueue);
|
||||
AddUIServices(services);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
@@ -173,7 +169,7 @@ public partial class App : Application, IDisposable
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
}
|
||||
|
||||
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)
|
||||
private static void AddUIServices(ServiceCollection services)
|
||||
{
|
||||
// Models
|
||||
var sm = SettingsModel.LoadSettings();
|
||||
@@ -192,8 +188,6 @@ public partial class App : Application, IDisposable
|
||||
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
services.AddSingleton<ResourceSwapper>();
|
||||
|
||||
services.AddIconServices(dispatcherQueue);
|
||||
}
|
||||
|
||||
private static void AddCoreServices(ServiceCollection services)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
Margin="4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
@@ -83,7 +83,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
Height="16"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SourceKey="{x:Bind Icon}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.WinUI.Deferred;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Deferred;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
@@ -16,11 +20,7 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
/// </summary>
|
||||
public partial class IconBox : ContentControl
|
||||
{
|
||||
private double _lastScale;
|
||||
private ElementTheme _lastTheme;
|
||||
private double _lastFontSize;
|
||||
|
||||
private const double DefaultIconFontSize = 16.0;
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IconSource"/> to display within the <see cref="IconBox"/>. Overwritten, if <see cref="SourceKey"/> is used instead.
|
||||
@@ -48,23 +48,10 @@ public partial class IconBox : ContentControl
|
||||
public static readonly DependencyProperty SourceKeyProperty =
|
||||
DependencyProperty.Register(nameof(SourceKey), typeof(object), typeof(IconBox), new PropertyMetadata(null, OnSourceKeyPropertyChanged));
|
||||
|
||||
private TypedEventHandler<IconBox, SourceRequestedEventArgs>? _sourceRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="SourceRequested"/> event handler to provide the value of the <see cref="IconSource"/> for the <see cref="Source"/> property from the provided <see cref="SourceKey"/>.
|
||||
/// </summary>
|
||||
public event TypedEventHandler<IconBox, SourceRequestedEventArgs>? SourceRequested
|
||||
{
|
||||
add
|
||||
{
|
||||
_sourceRequested += value;
|
||||
if (_sourceRequested?.GetInvocationList().Length == 1)
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
remove => _sourceRequested -= value;
|
||||
}
|
||||
public event TypedEventHandler<IconBox, SourceRequestedEventArgs>? SourceRequested;
|
||||
|
||||
public IconBox()
|
||||
{
|
||||
@@ -72,208 +59,119 @@ public partial class IconBox : ContentControl
|
||||
IsTabStop = false;
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center;
|
||||
VerticalContentAlignment = VerticalAlignment.Center;
|
||||
|
||||
Loaded += OnLoaded;
|
||||
Unloaded += OnUnloaded;
|
||||
ActualThemeChanged += OnActualThemeChanged;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
UpdateLastFontSize();
|
||||
}
|
||||
|
||||
private void UpdateLastFontSize()
|
||||
{
|
||||
_lastFontSize =
|
||||
Pick(Width)
|
||||
?? Pick(Height)
|
||||
?? Pick(ActualWidth)
|
||||
?? Pick(ActualHeight)
|
||||
?? DefaultIconFontSize;
|
||||
|
||||
return;
|
||||
|
||||
static double? Pick(double value) => double.IsFinite(value) && value > 0 ? value : null;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object s, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateLastFontSize();
|
||||
|
||||
if (Source is FontIconSource fontIcon)
|
||||
{
|
||||
fontIcon.FontSize = _lastFontSize;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
if (_lastTheme == ActualTheme)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastTheme = ActualTheme;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_lastTheme = ActualTheme;
|
||||
UpdateLastFontSize();
|
||||
|
||||
if (XamlRoot is not null)
|
||||
{
|
||||
_lastScale = XamlRoot.RasterizationScale;
|
||||
XamlRoot.Changed += OnXamlRootChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (XamlRoot is not null)
|
||||
{
|
||||
XamlRoot.Changed -= OnXamlRootChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnXamlRootChanged(XamlRoot sender, XamlRootChangedEventArgs args)
|
||||
{
|
||||
var newScale = sender.RasterizationScale;
|
||||
var changedLastTheme = _lastTheme != ActualTheme;
|
||||
_lastTheme = ActualTheme;
|
||||
if ((changedLastTheme || Math.Abs(newScale - _lastScale) > 0.01) && SourceKey is not null)
|
||||
{
|
||||
_lastScale = newScale;
|
||||
UpdateSourceKey(this, SourceKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (SourceKey is not null)
|
||||
{
|
||||
UpdateSourceKey(this, SourceKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not IconBox self)
|
||||
if (d is IconBox @this)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.NewValue)
|
||||
{
|
||||
case null:
|
||||
self.Content = null;
|
||||
self.Padding = default;
|
||||
break;
|
||||
case FontIconSource fontIcon:
|
||||
if (self.Content is IconSourceElement iconSourceElement)
|
||||
{
|
||||
iconSourceElement.IconSource = fontIcon;
|
||||
}
|
||||
else
|
||||
{
|
||||
fontIcon.FontSize = self._lastFontSize;
|
||||
switch (e.NewValue)
|
||||
{
|
||||
case null:
|
||||
@this.Content = null;
|
||||
break;
|
||||
case FontIconSource fontIco:
|
||||
fontIco.FontSize = double.IsNaN(@this.Width) ? @this.Height : @this.Width;
|
||||
|
||||
// For inexplicable reasons, FontIconSource.CreateIconElement
|
||||
// doesn't work, so do it ourselves
|
||||
// TODO: File platform bug?
|
||||
IconSourceElement elem = new()
|
||||
{
|
||||
IconSource = fontIco,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
IconSource = fontIcon,
|
||||
};
|
||||
self.Content = elem;
|
||||
}
|
||||
|
||||
self.Padding = new Thickness(Math.Round(self._lastFontSize * -0.2));
|
||||
|
||||
break;
|
||||
case BitmapIconSource bitmapIcon:
|
||||
if (self.Content is IconSourceElement iconSourceElement2)
|
||||
{
|
||||
iconSourceElement2.IconSource = bitmapIcon;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.Content = bitmapIcon.CreateIconElement();
|
||||
}
|
||||
|
||||
self.Padding = default;
|
||||
|
||||
break;
|
||||
case IconSource source:
|
||||
self.Content = source.CreateIconElement();
|
||||
self.Padding = default;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource.");
|
||||
@this.Content = elem;
|
||||
break;
|
||||
case IconSource source:
|
||||
@this.Content = source.CreateIconElement();
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnSourceKeyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not IconBox self)
|
||||
if (d is IconBox @this)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateSourceKey(self, e.NewValue);
|
||||
}
|
||||
|
||||
private static void UpdateSourceKey(IconBox iconBox, object? sourceKey)
|
||||
{
|
||||
if (sourceKey is null)
|
||||
{
|
||||
iconBox.Source = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Callback(iconBox, sourceKey);
|
||||
}
|
||||
|
||||
private static async void Callback(IconBox iconBox, object? sourceKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var iconBoxSourceRequestedHandler = iconBox._sourceRequested;
|
||||
|
||||
if (iconBoxSourceRequestedHandler is null)
|
||||
if (e.NewValue is null)
|
||||
{
|
||||
return;
|
||||
@this.Source = null;
|
||||
}
|
||||
|
||||
var eventArgs = new SourceRequestedEventArgs(sourceKey, iconBox._lastTheme, iconBox._lastScale);
|
||||
await iconBoxSourceRequestedHandler.InvokeAsync(iconBox, eventArgs);
|
||||
|
||||
// After the await:
|
||||
// Is the icon we're looking up now, the one we still
|
||||
// want to find? Since this IconBox might be used in a
|
||||
// list virtualization situation, it's very possible we
|
||||
// may have already been set to a new icon before we
|
||||
// even got back from the await.
|
||||
if (eventArgs.Key != sourceKey)
|
||||
else
|
||||
{
|
||||
// If the requested icon has changed, then just bail
|
||||
return;
|
||||
}
|
||||
// TODO GH #239 switch back when using the new MD text block
|
||||
// Switching back to EnqueueAsync has broken icons in tags (they don't show)
|
||||
// _ = @this._queue.EnqueueAsync(() =>
|
||||
@this._queue.TryEnqueue(async void () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (@this.SourceRequested is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventArgs.Value == iconBox.Source)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var requestedTheme = @this.ActualTheme;
|
||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||
|
||||
iconBox.Source = eventArgs.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Exception from TryEnqueue bypasses the global error handler,
|
||||
// and crashes the app.
|
||||
Logger.LogError("Failed to set icon", ex);
|
||||
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
|
||||
|
||||
// After the await:
|
||||
// Is the icon we're looking up now, the one we still
|
||||
// want to find? Since this IconBox might be used in a
|
||||
// list virtualization situation, it's very possible we
|
||||
// may have already been set to a new icon before we
|
||||
// even got back from the await.
|
||||
if (eventArgs.Key != @this.SourceKey)
|
||||
{
|
||||
// If the requested icon has changed, then just bail
|
||||
return;
|
||||
}
|
||||
|
||||
@this.Source = eventArgs.Value;
|
||||
|
||||
// Here's a little lesson in trickery:
|
||||
// Emoji are rendered just a bit bigger than Segoe Icons.
|
||||
// Just enough bigger that they get clipped if you put
|
||||
// them in a box at the same size.
|
||||
//
|
||||
// So, if the icon we get back was a font icon,
|
||||
// and the glyph for that icon is NOT in the range of
|
||||
// Segoe icons, then let's give the icon some extra space
|
||||
var iconData = eventArgs.Key switch
|
||||
{
|
||||
IconDataViewModel key => key,
|
||||
IconInfoViewModel info => requestedTheme == ElementTheme.Light ? info.Light : info.Dark,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (iconData?.Icon is not null && @this.Source is FontIconSource)
|
||||
{
|
||||
var iconSize =
|
||||
!double.IsNaN(@this.Width) ? @this.Width :
|
||||
!double.IsNaN(@this.Height) ? @this.Height :
|
||||
@this.ActualWidth > 0 ? @this.ActualWidth :
|
||||
@this.ActualHeight;
|
||||
|
||||
@this.Padding = new Thickness(Math.Round(iconSize * -0.2));
|
||||
}
|
||||
else
|
||||
{
|
||||
@this.Padding = default;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Exception from TryEnqueue bypasses the global error handler,
|
||||
// and crashes the app.
|
||||
Logger.LogError("Failed to set icon", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,11 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
/// <summary>
|
||||
/// See <see cref="IconBox.SourceRequested"/> event.
|
||||
/// </summary>
|
||||
public class SourceRequestedEventArgs(object? key, ElementTheme requestedTheme, double scale = 1.0) : DeferredEventArgs
|
||||
public class SourceRequestedEventArgs(object? key, ElementTheme requestedTheme) : DeferredEventArgs
|
||||
{
|
||||
public object? Key { get; private set; } = key;
|
||||
|
||||
public IconSource? Value { get; set; }
|
||||
|
||||
public ElementTheme Theme => requestedTheme;
|
||||
|
||||
public double Scale => scale;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
<local:IconBox
|
||||
x:Name="PART_Icon"
|
||||
Grid.Column="0"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="{Binding Text, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IconMarginConverter}}"
|
||||
SourceKey="{TemplateBinding Icon}" />
|
||||
|
||||
@@ -72,7 +72,7 @@ public partial class Tag : Control
|
||||
|
||||
if (GetTemplateChild(TagIconBox) is IconBox iconBox)
|
||||
{
|
||||
iconBox.SourceRequested += IconCacheProvider.SourceRequested20;
|
||||
iconBox.SourceRequested += IconCacheProvider.SourceRequested;
|
||||
iconBox.Visibility = HasIcon ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,6 @@
|
||||
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
|
||||
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
||||
|
||||
<x:Double x:Key="SmallGridSize">32</x:Double>
|
||||
<x:Double x:Key="MediumGridSize">48</x:Double>
|
||||
<x:Double x:Key="MediumGridContainerSize">100</x:Double>
|
||||
<x:Double x:Key="GalleryGridSize">160</x:Double>
|
||||
|
||||
<x:Double x:Key="ListViewItemMinHeight">40</x:Double>
|
||||
<x:Double x:Key="ListViewSectionMinHeight">0</x:Double>
|
||||
<x:Double x:Key="ListViewSeparatorMinHeight">0</x:Double>
|
||||
@@ -293,7 +288,7 @@
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
@@ -382,21 +377,21 @@
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="GridIconBorder"
|
||||
Width="{StaticResource SmallGridSize}"
|
||||
Height="{StaticResource SmallGridSize}"
|
||||
Width="28"
|
||||
Height="28"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested32}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
Width="{StaticResource MediumGridContainerSize}"
|
||||
Height="{StaticResource MediumGridContainerSize}"
|
||||
Width="100"
|
||||
Height="100"
|
||||
Padding="8"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
|
||||
@@ -408,15 +403,15 @@
|
||||
<cpcontrols:IconBox
|
||||
x:Name="GridIconBorder"
|
||||
Grid.Row="0"
|
||||
Width="{StaticResource MediumGridSize}"
|
||||
Height="{StaticResource MediumGridSize}"
|
||||
Width="36"
|
||||
Height="36"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Row="1"
|
||||
@@ -435,7 +430,7 @@
|
||||
|
||||
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<StackPanel
|
||||
Width="{StaticResource GalleryGridSize}"
|
||||
Width="160"
|
||||
Margin="4"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -447,8 +442,8 @@
|
||||
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
|
||||
|
||||
<Grid
|
||||
Width="{StaticResource GalleryGridSize}"
|
||||
Height="{StaticResource GalleryGridSize}"
|
||||
Width="160"
|
||||
Height="160"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
@@ -461,7 +456,7 @@
|
||||
CornerRadius="4"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested256}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
|
||||
@@ -587,7 +582,7 @@
|
||||
Margin="8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -1,299 +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.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance, near-lock-free adaptive cache optimized for UI Icons.
|
||||
/// Eviction merely drops references to allow the GC to manage UI-bound lifetimes.
|
||||
/// </summary>
|
||||
internal sealed class AdaptiveCache<TKey, TValue>
|
||||
where TKey : IEquatable<TKey>
|
||||
{
|
||||
private readonly int _capacity;
|
||||
private readonly double _decayFactor;
|
||||
private readonly TimeSpan _decayInterval;
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, CacheEntry> _map;
|
||||
private readonly ConcurrentStack<CacheEntry> _pool = [];
|
||||
private readonly WaitCallback _maintenanceCallback;
|
||||
|
||||
private long _currentTick;
|
||||
private long _lastDecayTicks = DateTime.UtcNow.Ticks;
|
||||
private InterlockedBoolean _maintenanceSwitch = new(false);
|
||||
|
||||
public AdaptiveCache(int capacity = 384, TimeSpan? decayInterval = null, double decayFactor = 0.5)
|
||||
{
|
||||
_capacity = capacity;
|
||||
_decayInterval = decayInterval ?? TimeSpan.FromMinutes(5);
|
||||
_decayFactor = decayFactor;
|
||||
_map = new ConcurrentDictionary<TKey, CacheEntry>(Environment.ProcessorCount, capacity);
|
||||
|
||||
_maintenanceCallback = static state =>
|
||||
{
|
||||
var cache = (AdaptiveCache<TKey, TValue>)state!;
|
||||
try
|
||||
{
|
||||
cache.PerformCleanup();
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache._maintenanceSwitch.Clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public TValue GetOrAdd<TArg>(TKey key, Func<TKey, TArg, TValue> factory, TArg arg)
|
||||
{
|
||||
if (_map.TryGetValue(key, out var entry))
|
||||
{
|
||||
entry.Update(Interlocked.Increment(ref _currentTick));
|
||||
return entry.Value!;
|
||||
}
|
||||
|
||||
if (!_pool.TryPop(out var newEntry))
|
||||
{
|
||||
newEntry = new CacheEntry();
|
||||
}
|
||||
|
||||
var value = factory(key, arg);
|
||||
var tick = Interlocked.Increment(ref _currentTick);
|
||||
newEntry.Initialize(key, value, 1.0, tick);
|
||||
|
||||
if (!_map.TryAdd(key, newEntry))
|
||||
{
|
||||
newEntry.Clear();
|
||||
_pool.Push(newEntry);
|
||||
|
||||
if (_map.TryGetValue(key, out var existing))
|
||||
{
|
||||
existing.Update(tick);
|
||||
return existing.Value!;
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldMaintenanceRun())
|
||||
{
|
||||
TryRunMaintenance();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public bool TryGet(TKey key, [MaybeNullWhen(false)] out TValue value)
|
||||
{
|
||||
if (_map.TryGetValue(key, out var entry))
|
||||
{
|
||||
entry.Update(Interlocked.Increment(ref _currentTick));
|
||||
value = entry.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Add(TKey key, TValue value)
|
||||
{
|
||||
var tick = Interlocked.Increment(ref _currentTick);
|
||||
|
||||
if (_map.TryGetValue(key, out var existing))
|
||||
{
|
||||
existing.Update(tick);
|
||||
existing.SetValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_pool.TryPop(out var newEntry))
|
||||
{
|
||||
newEntry = new CacheEntry();
|
||||
}
|
||||
|
||||
newEntry.Initialize(key, value, 1.0, tick);
|
||||
|
||||
if (!_map.TryAdd(key, newEntry))
|
||||
{
|
||||
newEntry.Clear();
|
||||
_pool.Push(newEntry);
|
||||
}
|
||||
|
||||
if (ShouldMaintenanceRun())
|
||||
{
|
||||
TryRunMaintenance();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRemove(TKey key)
|
||||
{
|
||||
if (_map.TryRemove(key, out var evicted))
|
||||
{
|
||||
evicted.Clear();
|
||||
_pool.Push(evicted);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var key in _map.Keys)
|
||||
{
|
||||
TryRemove(key);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _currentTick, 0);
|
||||
}
|
||||
|
||||
private bool ShouldMaintenanceRun()
|
||||
{
|
||||
return _map.Count > _capacity || (DateTime.UtcNow.Ticks - Interlocked.Read(ref _lastDecayTicks)) > _decayInterval.Ticks;
|
||||
}
|
||||
|
||||
private void TryRunMaintenance()
|
||||
{
|
||||
if (_maintenanceSwitch.Set())
|
||||
{
|
||||
ThreadPool.UnsafeQueueUserWorkItem(_maintenanceCallback, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformCleanup()
|
||||
{
|
||||
var nowTicks = DateTime.UtcNow.Ticks;
|
||||
var isDecay = (nowTicks - Interlocked.Read(ref _lastDecayTicks)) > _decayInterval.Ticks;
|
||||
if (isDecay)
|
||||
{
|
||||
Interlocked.Exchange(ref _lastDecayTicks, nowTicks);
|
||||
}
|
||||
|
||||
var currentTick = Interlocked.Read(ref _currentTick);
|
||||
|
||||
foreach (var (key, entry) in _map)
|
||||
{
|
||||
if (isDecay)
|
||||
{
|
||||
entry.Decay(_decayFactor);
|
||||
}
|
||||
|
||||
var score = CalculateScore(entry, currentTick);
|
||||
|
||||
if (score < 0.1 || _map.Count > _capacity)
|
||||
{
|
||||
if (_map.TryRemove(key, out var evicted))
|
||||
{
|
||||
evicted.Clear();
|
||||
_pool.Push(evicted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the survival score of an entry.
|
||||
/// Higher score = stay in cache; Lower score = priority for eviction.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double CalculateScore(CacheEntry entry, long currentTick)
|
||||
{
|
||||
// Tuning parameter: How much weight to give recency vs frequency.
|
||||
// - a larger ageWeight makes the cache behave more like LRU (Least Recently Used).
|
||||
// - a smaller ageWeight makes it behave more like LFU (Least Frequently Used).
|
||||
const double ageWeight = 0.001;
|
||||
|
||||
var frequency = entry.GetFrequency();
|
||||
var age = currentTick - entry.GetLastAccess();
|
||||
|
||||
return frequency - (age * ageWeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single pooled entry in the cache, containing the value and
|
||||
/// atomic metadata for adaptive eviction logic.
|
||||
/// </summary>
|
||||
private sealed class CacheEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the key associated with this entry. Used primarily for identification during cleanup.
|
||||
/// </summary>
|
||||
public TKey Key { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cached value. This reference is cleared on eviction to allow GC collection.
|
||||
/// </summary>
|
||||
public TValue Value { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the frequency count as double bits to allow for Interlocked atomic math.
|
||||
/// Frequencies are decayed over time to ensure the cache adapts to new usage patterns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This allows the use of Interlocked.CompareExchange to perform thread-safe floating point
|
||||
/// arithmetic without a global lock.
|
||||
/// </remarks>
|
||||
private long _frequencyBits;
|
||||
|
||||
/// <summary>
|
||||
/// The tick (monotonically increasing counter) of the last time this entry was accessed.
|
||||
/// </summary>
|
||||
private long _lastAccessTick;
|
||||
|
||||
public void Initialize(TKey key, TValue value, double frequency, long lastAccessTick)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
_frequencyBits = BitConverter.DoubleToInt64Bits(frequency);
|
||||
_lastAccessTick = lastAccessTick;
|
||||
}
|
||||
|
||||
public void SetValue(TValue value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Key = default!;
|
||||
Value = default!;
|
||||
}
|
||||
|
||||
public void Update(long tick)
|
||||
{
|
||||
Interlocked.Exchange(ref _lastAccessTick, tick);
|
||||
long initial, updated;
|
||||
do
|
||||
{
|
||||
initial = Interlocked.Read(ref _frequencyBits);
|
||||
updated = BitConverter.DoubleToInt64Bits(BitConverter.Int64BitsToDouble(initial) + 1.0);
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref _frequencyBits, updated, initial) != initial);
|
||||
}
|
||||
|
||||
public void Decay(double factor)
|
||||
{
|
||||
long initial, updated;
|
||||
do
|
||||
{
|
||||
initial = Interlocked.Read(ref _frequencyBits);
|
||||
updated = BitConverter.DoubleToInt64Bits(BitConverter.Int64BitsToDouble(initial) * factor);
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref _frequencyBits, updated, initial) != initial);
|
||||
}
|
||||
|
||||
public double GetFrequency()
|
||||
{
|
||||
return BitConverter.Int64BitsToDouble(Interlocked.Read(ref _frequencyBits));
|
||||
}
|
||||
|
||||
public long GetLastAccess()
|
||||
{
|
||||
return Interlocked.Read(ref _lastAccessTick);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
|
||||
/// </summary>
|
||||
public static partial class IconCacheProvider
|
||||
{
|
||||
private static readonly IconCacheService IconService = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
|
||||
|
||||
#pragma warning disable IDE0060 // Remove unused parameter
|
||||
public static async void SourceRequested(IconBox sender, SourceRequestedEventArgs args)
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
{
|
||||
if (args.Key is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Key is IconDataViewModel iconData)
|
||||
{
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
args.Value = await IconService.GetIconSource(iconData);
|
||||
|
||||
deferral.Complete();
|
||||
}
|
||||
else if (args.Key is IconInfoViewModel iconInfo)
|
||||
{
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
var data = args.Theme == Microsoft.UI.Xaml.ElementTheme.Dark ? iconInfo.Dark : iconInfo.Light;
|
||||
args.Value = await IconService.GetIconSource(data);
|
||||
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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.Diagnostics;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
public Task<IconSource?> GetIconSource(IconDataViewModel icon) =>
|
||||
|
||||
// todo: actually implement a cache of some sort
|
||||
IconToSource(icon);
|
||||
|
||||
private async Task<IconSource?> IconToSource(IconDataViewModel icon)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(icon.Icon))
|
||||
{
|
||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false, icon.FontFamily);
|
||||
return source;
|
||||
}
|
||||
else if (icon.Data is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await StreamToIconSource(icon.Data.Unsafe!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("Failed to load icon from stream: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<IconSource?> StreamToIconSource(IRandomAccessStreamReference iconStreamRef)
|
||||
{
|
||||
if (iconStreamRef is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bitmap = await IconStreamToBitmapImageAsync(iconStreamRef);
|
||||
var icon = new ImageIconSource() { ImageSource = bitmap };
|
||||
return icon;
|
||||
}
|
||||
|
||||
private async Task<BitmapImage> IconStreamToBitmapImageAsync(IRandomAccessStreamReference iconStreamRef)
|
||||
{
|
||||
// Return the bitmap image via TaskCompletionSource. Using WCT's EnqueueAsync does not suffice here, since if
|
||||
// we're already on the thread of the DispatcherQueue then it just directly calls the function, with no async involved.
|
||||
return await TryEnqueueAsync(dispatcherQueue, async () =>
|
||||
{
|
||||
using var bitmapStream = await iconStreamRef.OpenReadAsync();
|
||||
var itemImage = new BitmapImage();
|
||||
await itemImage.SetSourceAsync(bitmapStream);
|
||||
return itemImage;
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<T> TryEnqueueAsync<T>(DispatcherQueue dispatcher, Func<Task<T>> function)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource<T>();
|
||||
|
||||
var enqueued = dispatcher.TryEnqueue(DispatcherQueuePriority.Normal, async void () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await function();
|
||||
completionSource.SetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completionSource.SetException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
if (!enqueued)
|
||||
{
|
||||
completionSource.SetException(new InvalidOperationException("Failed to enqueue the operation on the UI dispatcher"));
|
||||
}
|
||||
|
||||
return completionSource.Task;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +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.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed class CachedIconSourceProvider : IIconSourceProvider
|
||||
{
|
||||
private readonly AdaptiveCache<IconCacheKey, Task<IconSource?>> _cache;
|
||||
private readonly Size _iconSize;
|
||||
private readonly IconLoaderService _loader;
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
public CachedIconSourceProvider(IconLoaderService loader, Size iconSize, int cacheSize)
|
||||
{
|
||||
_loader = loader;
|
||||
_iconSize = iconSize;
|
||||
_cache = new AdaptiveCache<IconCacheKey, Task<IconSource?>>(cacheSize, TimeSpan.FromMinutes(60));
|
||||
}
|
||||
|
||||
public CachedIconSourceProvider(IconLoaderService loader, int iconSize, int cacheSize)
|
||||
: this(loader, new Size(iconSize, iconSize), cacheSize)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<IconSource?> GetIconSource(IconDataViewModel icon, double scale)
|
||||
{
|
||||
var key = new IconCacheKey(icon, scale);
|
||||
|
||||
return _cache.TryGet(key, out var existingTask)
|
||||
? existingTask
|
||||
: GetOrCreateSlowPath(key, icon, scale);
|
||||
}
|
||||
|
||||
private Task<IconSource?> GetOrCreateSlowPath(IconCacheKey key, IconDataViewModel icon, double scale)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_cache.TryGet(key, out var existingTask))
|
||||
{
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<IconSource?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
_loader.EnqueueLoad(
|
||||
icon.Icon,
|
||||
icon.FontFamily,
|
||||
icon.Data?.Unsafe,
|
||||
_iconSize,
|
||||
scale,
|
||||
tcs);
|
||||
|
||||
var task = tcs.Task;
|
||||
|
||||
_ = task.ContinueWith(
|
||||
_ =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_cache.TryRemove(key);
|
||||
}
|
||||
},
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
_cache.Add(key, task);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct IconCacheKey : IEquatable<IconCacheKey>
|
||||
{
|
||||
private readonly string? _icon;
|
||||
private readonly string? _fontFamily;
|
||||
private readonly int _streamRefHashCode;
|
||||
private readonly int _scale;
|
||||
|
||||
public IconCacheKey(IconDataViewModel icon, double scale)
|
||||
{
|
||||
_icon = icon.Icon;
|
||||
_fontFamily = icon.FontFamily;
|
||||
_streamRefHashCode = icon.Data?.Unsafe is { } stream
|
||||
? RuntimeHelpers.GetHashCode(stream)
|
||||
: 0;
|
||||
_scale = (int)(100 * Math.Round(scale, 2));
|
||||
}
|
||||
|
||||
public bool Equals(IconCacheKey other) =>
|
||||
_icon == other._icon &&
|
||||
_fontFamily == other._fontFamily &&
|
||||
_streamRefHashCode == other._streamRefHashCode &&
|
||||
_scale == other._scale;
|
||||
|
||||
public override bool Equals(object? obj) => obj is IconCacheKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(_icon, _fontFamily, _streamRefHashCode, _scale);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +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.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal interface IIconLoaderService : IAsyncDisposable
|
||||
{
|
||||
void EnqueueLoad(
|
||||
string? iconString,
|
||||
string? fontFamily,
|
||||
IRandomAccessStreamReference? streamRef,
|
||||
Size iconSize,
|
||||
double scale,
|
||||
TaskCompletionSource<IconSource?> tcs,
|
||||
IconLoadPriority priority);
|
||||
}
|
||||
@@ -1,13 +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.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal interface IIconSourceProvider
|
||||
{
|
||||
Task<IconSource?> GetIconSource(IconDataViewModel icon, double scale);
|
||||
}
|
||||
@@ -1,79 +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.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
|
||||
/// </summary>
|
||||
public static partial class IconCacheProvider
|
||||
{
|
||||
/*
|
||||
Memory Usage Considerations (raw estimates):
|
||||
| Icon Size | Per Icon | Count | Total | Per Icon @ 200% | Total @ 200% | Per Icon @ 300% | Total @ 300% |
|
||||
| --------- | -------: | ----: | -------: | --------------: | -----------: | --------------: | -----------: |
|
||||
| 20×20 | 1.6 KB | 1024 | 1.6 MB | 6.4 KB | 6.4 MB | 14.4 KB | 14.4 MB |
|
||||
| 32×32 | 4.0 KB | 512 | 2.0 MB | 16 KB | 8.0 MB | 36.0 KB | 18.0 MB |
|
||||
| 48×48 | 9.0 KB | 256 | 2.3 MB | 36 KB | 9.0 MB | 81.0 KB | 20.3 MB |
|
||||
| 64×64 | 16.0 KB | 64 | 1.0 MB | 64 KB | 4.0 MB | 144.0 KB | 9.0 MB |
|
||||
| 256×256 | 256.0 KB | 64 | 16.0 MB | 1 MB | 64.0 MB | 2.3 MB | 144 MB |
|
||||
*/
|
||||
|
||||
private static IIconSourceProvider _provider20 = null!;
|
||||
private static IIconSourceProvider _provider32 = null!;
|
||||
private static IIconSourceProvider _provider64 = null!;
|
||||
private static IIconSourceProvider _provider256 = null!;
|
||||
|
||||
public static void Initialize(IServiceProvider serviceProvider)
|
||||
{
|
||||
_provider20 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size20);
|
||||
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
|
||||
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
|
||||
_provider256 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size256);
|
||||
}
|
||||
|
||||
private static async void SourceRequestedCore(IIconSourceProvider service, SourceRequestedEventArgs args)
|
||||
{
|
||||
if (args.Key is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
try
|
||||
{
|
||||
args.Value = args.Key switch
|
||||
{
|
||||
IconDataViewModel iconData => await service.GetIconSource(iconData, args.Scale),
|
||||
IconInfoViewModel iconInfo => await service.GetIconSource(
|
||||
args.Theme == Microsoft.UI.Xaml.ElementTheme.Light ? iconInfo.Light : iconInfo.Dark,
|
||||
args.Scale),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0060 // Remove unused parameter
|
||||
public static void SourceRequested20(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider20, args);
|
||||
|
||||
public static void SourceRequested32(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider32, args);
|
||||
|
||||
public static void SourceRequested64(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider64, args);
|
||||
|
||||
public static void SourceRequested256(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider256, args);
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
}
|
||||
@@ -1,11 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal enum IconLoadPriority
|
||||
{
|
||||
Low,
|
||||
High,
|
||||
}
|
||||
@@ -1,224 +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.
|
||||
|
||||
using System.Threading.Channels;
|
||||
using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.Foundation;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
{
|
||||
private const DispatcherQueuePriority LoadingPriorityOnDispatcher = DispatcherQueuePriority.Low;
|
||||
private const int DefaultIconSize = 256;
|
||||
private const int MaxWorkerCount = 4;
|
||||
|
||||
private static readonly int WorkerCount = Math.Clamp(Environment.ProcessorCount / 2, 1, MaxWorkerCount);
|
||||
|
||||
private readonly Channel<Func<Task>> _highPriorityQueue = Channel.CreateBounded<Func<Task>>(32);
|
||||
private readonly Channel<Func<Task>> _lowPriorityQueue = Channel.CreateUnbounded<Func<Task>>();
|
||||
private readonly Task[] _workers;
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
public IconLoaderService(DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
_dispatcherQueue = dispatcherQueue;
|
||||
_workers = new Task[WorkerCount];
|
||||
|
||||
for (var i = 0; i < WorkerCount; i++)
|
||||
{
|
||||
_workers[i] = Task.Run(ProcessQueueAsync);
|
||||
}
|
||||
}
|
||||
|
||||
public void EnqueueLoad(
|
||||
string? iconString,
|
||||
string? fontFamily,
|
||||
IRandomAccessStreamReference? streamRef,
|
||||
Size iconSize,
|
||||
double scale,
|
||||
TaskCompletionSource<IconSource?> tcs,
|
||||
IconLoadPriority priority = IconLoadPriority.Low)
|
||||
{
|
||||
var workItem = () => LoadAndCompleteAsync(iconString, fontFamily, streamRef, iconSize, scale, tcs);
|
||||
|
||||
if (priority == IconLoadPriority.High)
|
||||
{
|
||||
if (_highPriorityQueue.Writer.TryWrite(workItem))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Logger.LogDebug("High priority icon queue full, falling back to low priority");
|
||||
#endif
|
||||
}
|
||||
|
||||
_lowPriorityQueue.Writer.TryWrite(workItem);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_highPriorityQueue.Writer.Complete();
|
||||
_lowPriorityQueue.Writer.Complete();
|
||||
|
||||
await Task.WhenAll(_workers).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ProcessQueueAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Func<Task>? workItem;
|
||||
|
||||
if (_highPriorityQueue.Reader.TryRead(out workItem))
|
||||
{
|
||||
await ExecuteWork(workItem).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var highWait = _highPriorityQueue.Reader.WaitToReadAsync().AsTask();
|
||||
var lowWait = _lowPriorityQueue.Reader.WaitToReadAsync().AsTask();
|
||||
|
||||
await Task.WhenAny(highWait, lowWait).ConfigureAwait(false);
|
||||
|
||||
// Check if both channels are completed (disposal)
|
||||
if (_highPriorityQueue.Reader.Completion.IsCompleted &&
|
||||
_lowPriorityQueue.Reader.Completion.IsCompleted)
|
||||
{
|
||||
// Drain any remaining items
|
||||
while (_highPriorityQueue.Reader.TryRead(out workItem))
|
||||
{
|
||||
await ExecuteWork(workItem).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
while (_lowPriorityQueue.Reader.TryRead(out workItem))
|
||||
{
|
||||
await ExecuteWork(workItem).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (_highPriorityQueue.Reader.TryRead(out workItem) ||
|
||||
_lowPriorityQueue.Reader.TryRead(out workItem))
|
||||
{
|
||||
await ExecuteWork(workItem).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task ExecuteWork(Func<Task> workItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
await workItem().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load icon", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAndCompleteAsync(
|
||||
string? iconString,
|
||||
string? fontFamily,
|
||||
IRandomAccessStreamReference? streamRef,
|
||||
Size iconSize,
|
||||
double scale,
|
||||
TaskCompletionSource<IconSource?> tcs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await LoadIconCoreAsync(iconString, fontFamily, streamRef, iconSize, scale).ConfigureAwait(false);
|
||||
tcs.TrySetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IconSource?> LoadIconCoreAsync(
|
||||
string? iconString,
|
||||
string? fontFamily,
|
||||
IRandomAccessStreamReference? streamRef,
|
||||
Size iconSize,
|
||||
double scale)
|
||||
{
|
||||
var scaledSize = new Size(iconSize.Width * scale, iconSize.Height * scale);
|
||||
|
||||
if (!string.IsNullOrEmpty(iconString))
|
||||
{
|
||||
return await _dispatcherQueue
|
||||
.EnqueueAsync(() => GetStringIconSource(iconString, fontFamily, scaledSize), LoadingPriorityOnDispatcher)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (streamRef != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var bitmapStream = await streamRef.OpenReadAsync().AsTask().ConfigureAwait(false);
|
||||
|
||||
return await _dispatcherQueue
|
||||
.EnqueueAsync(BuildImageSource, LoadingPriorityOnDispatcher)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
async Task<IconSource?> BuildImageSource()
|
||||
{
|
||||
var bitmap = new BitmapImage();
|
||||
ApplyDecodeSize(bitmap, scaledSize);
|
||||
await bitmap.SetSourceAsync(bitmapStream);
|
||||
return new ImageIconSource { ImageSource = bitmap };
|
||||
}
|
||||
}
|
||||
#pragma warning disable CS0168 // Variable is declared but never used
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CS0168 // Variable is declared but never used
|
||||
{
|
||||
#if DEBUG
|
||||
Logger.LogDebug($"Failed to open icon stream: {ex}");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ApplyDecodeSize(BitmapImage bitmap, Size size)
|
||||
{
|
||||
if (size.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (size.Width >= size.Height)
|
||||
{
|
||||
bitmap.DecodePixelWidth = (int)size.Width;
|
||||
}
|
||||
else
|
||||
{
|
||||
bitmap.DecodePixelHeight = (int)size.Height;
|
||||
}
|
||||
}
|
||||
|
||||
private static IconSource? GetStringIconSource(string iconString, string? fontFamily, Size size)
|
||||
{
|
||||
var iconSize = (int)Math.Max(size.Width, size.Height);
|
||||
if (iconSize == 0)
|
||||
{
|
||||
iconSize = DefaultIconSize;
|
||||
}
|
||||
|
||||
return IconPathConverter.IconSourceMUX(iconString, false, fontFamily, iconSize);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +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.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal static class IconServiceRegistration
|
||||
{
|
||||
public static IServiceCollection AddIconServices(this IServiceCollection services, DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
// Single shared loader
|
||||
var loader = new IconLoaderService(dispatcherQueue);
|
||||
services.AddSingleton<IIconLoaderService>(loader);
|
||||
|
||||
// Keyed providers by size
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size20,
|
||||
(_, _) => new CachedIconSourceProvider(loader, 20, 1024));
|
||||
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size32,
|
||||
(_, _) => new IconSourceProvider(loader, 32));
|
||||
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size64,
|
||||
(_, _) => new CachedIconSourceProvider(loader, 64, 256));
|
||||
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size256,
|
||||
(_, _) => new CachedIconSourceProvider(loader, 256, 64));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +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.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed class IconSourceProvider : IIconSourceProvider
|
||||
{
|
||||
private readonly IconLoaderService _loader;
|
||||
private readonly Size _iconSize;
|
||||
|
||||
public IconSourceProvider(IconLoaderService loader, Size iconSize)
|
||||
{
|
||||
_loader = loader;
|
||||
_iconSize = iconSize;
|
||||
}
|
||||
|
||||
public IconSourceProvider(IconLoaderService loader, int iconSize)
|
||||
: this(loader, new Size(iconSize, iconSize))
|
||||
{
|
||||
}
|
||||
|
||||
public Task<IconSource?> GetIconSource(IconDataViewModel icon, double scale)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<IconSource?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
_loader.EnqueueLoad(
|
||||
icon.Icon,
|
||||
icon.FontFamily,
|
||||
icon.Data?.Unsafe,
|
||||
_iconSize,
|
||||
scale,
|
||||
tcs);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal enum WellKnownIconSize
|
||||
{
|
||||
Size20 = 20,
|
||||
Size32 = 32,
|
||||
Size64 = 64,
|
||||
Size256 = 256,
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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 CommunityToolkit.Common.Deferred;
|
||||
using Windows.Foundation;
|
||||
|
||||
// Pilfered from CommunityToolkit.WinUI.Deferred
|
||||
namespace Microsoft.CmdPal.UI.Deferred;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions to <see cref="TypedEventHandler{TSender, TResult}"/> for Deferred Events.
|
||||
/// </summary>
|
||||
public static class TypedEventHandlerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Use to invoke an async <see cref="TypedEventHandler{TSender, TResult}"/> using <see cref="DeferredEventArgs"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="S">Type of sender.</typeparam>
|
||||
/// <typeparam name="R"><see cref="EventArgs"/> type.</typeparam>
|
||||
/// <param name="eventHandler"><see cref="TypedEventHandler{TSender, TResult}"/> to be invoked.</param>
|
||||
/// <param name="sender">Sender of the event.</param>
|
||||
/// <param name="eventArgs"><see cref="EventArgs"/> instance.</param>
|
||||
/// <returns><see cref="Task"/> to wait on deferred event handler.</returns>
|
||||
#pragma warning disable CA1715 // Identifiers should have correct prefix
|
||||
#pragma warning disable SA1314 // Type parameter names should begin with T
|
||||
public static Task InvokeAsync<S, R>(this TypedEventHandler<S, R> eventHandler, S sender, R eventArgs)
|
||||
#pragma warning restore SA1314 // Type parameter names should begin with T
|
||||
#pragma warning restore CA1715 // Identifiers should have correct prefix
|
||||
where R : DeferredEventArgs => InvokeAsync(eventHandler, sender, eventArgs, CancellationToken.None);
|
||||
|
||||
/// <summary>
|
||||
/// Use to invoke an async <see cref="TypedEventHandler{TSender, TResult}"/> using <see cref="DeferredEventArgs"/> with a <see cref="CancellationToken"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="S">Type of sender.</typeparam>
|
||||
/// <typeparam name="R"><see cref="EventArgs"/> type.</typeparam>
|
||||
/// <param name="eventHandler"><see cref="TypedEventHandler{TSender, TResult}"/> to be invoked.</param>
|
||||
/// <param name="sender">Sender of the event.</param>
|
||||
/// <param name="eventArgs"><see cref="EventArgs"/> instance.</param>
|
||||
/// <param name="cancellationToken"><see cref="CancellationToken"/> option.</param>
|
||||
/// <returns><see cref="Task"/> to wait on deferred event handler.</returns>
|
||||
#pragma warning disable CA1715 // Identifiers should have correct prefix
|
||||
#pragma warning disable SA1314 // Type parameter names should begin with T
|
||||
public static Task InvokeAsync<S, R>(this TypedEventHandler<S, R> eventHandler, S sender, R eventArgs, CancellationToken cancellationToken)
|
||||
#pragma warning restore SA1314 // Type parameter names should begin with T
|
||||
#pragma warning restore CA1715 // Identifiers should have correct prefix
|
||||
where R : DeferredEventArgs
|
||||
{
|
||||
if (eventHandler is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var tasks = eventHandler.GetInvocationList()
|
||||
.OfType<TypedEventHandler<S, R>>()
|
||||
.Select(invocationDelegate =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
invocationDelegate(sender, eventArgs);
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var deferral = eventArgs.GetCurrentDeferralAndReset();
|
||||
|
||||
return deferral?.WaitForCompletion(cancellationToken) ?? Task.CompletedTask;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
@@ -648,14 +648,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// Updates our window s.t. the top of the window is draggable.
|
||||
private void UpdateRegionsForCustomTitleBar()
|
||||
{
|
||||
var xamlRoot = RootElement.XamlRoot;
|
||||
if (xamlRoot is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Specify the interactive regions of the title bar.
|
||||
var scaleAdjustment = xamlRoot.RasterizationScale;
|
||||
var scaleAdjustment = RootElement.XamlRoot.RasterizationScale;
|
||||
|
||||
// Get the rectangle around our XAML content. We're going to mark this
|
||||
// rectangle as "Passthrough", so that the normal window operations
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user