Compare commits

..

1 Commits

Author SHA1 Message Date
Gordon Lam (SH)
f341fc7f10 fix: address issue #10675 2026-02-04 20:37:37 -08:00
400 changed files with 2481 additions and 45652 deletions

View File

@@ -1 +0,0 @@
../.github/copilot-instructions.md

View File

@@ -1 +0,0 @@
../.github/agents

View File

@@ -1 +0,0 @@
../.github/prompts

View File

@@ -1 +0,0 @@
../.github/instructions

View File

@@ -1 +0,0 @@
../.github/skills

View File

@@ -1,63 +0,0 @@
acq
APPLYTOSUBMENUS
AUDCLNT
bitmaps
BUFFERFLAGS
centiseconds
Ctl
CTLCOLOR
CTLCOLORBTN
CTLCOLORDLG
CTLCOLOREDIT
CTLCOLORLISTBOX
CTrim
DFCS
dlg
dlu
DONTCARE
DRAWITEM
DRAWITEMSTRUCT
DWLP
EDITCONTROL
ENABLEHOOK
FDE
GETCHANNELRECT
GETCHECK
GETTHUMBRECT
GIFs
HTBOTTOMRIGHT
HTHEME
KSDATAFORMAT
LEFTNOWORDWRAP
letterbox
lld
logfont
lround
MENUINFO
mic
MMRESULT
OWNERDRAW
PBGRA
pfdc
playhead
pwfx
quantums
REFKNOWNFOLDERID
reposted
SCROLLSIZEGRIP
SETDEFID
SETRECT
SHAREMODE
SHAREVIOLATION
STREAMFLAGS
submix
tci
TEXTMETRIC
tme
TRACKMOUSEEVENT
Unadvise
WASAPI
WAVEFORMATEX
WAVEFORMATEXTENSIBLE
wil
WMU

View File

@@ -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/

View File

@@ -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
@@ -664,7 +635,6 @@ GMEM
GNumber
googleai
googlegemini
Gotchas
gpedit
gpo
GPOCA
@@ -677,13 +647,13 @@ GSM
gtm
guiddata
GUITHREADINFO
Gotcha
Gotchas
GValue
gwl
GWLP
GWLSTYLE
hangeul
Hann
Hantai
Hanzi
Hardlines
hardlinks
@@ -742,7 +712,6 @@ HKPD
HKU
HMD
hmenu
HMON
hmodule
hmonitor
homies
@@ -760,7 +729,6 @@ hotkeys
hotlight
hotspot
HPAINTBUFFER
HPhysical
HRAWINPUT
hredraw
hres
@@ -771,7 +739,6 @@ hsb
HSCROLL
hsi
HSpeed
HSync
HTCLIENT
hthumbnail
HTOUCHINPUT
@@ -781,7 +748,6 @@ HVal
HValue
Hvci
hwb
HWP
HWHEEL
HWINEVENTHOOK
hwnd
@@ -795,7 +761,6 @@ IAI
icf
ICONERROR
ICONLOCATION
ICONONLY
IDCANCEL
IDD
idk
@@ -839,7 +804,6 @@ INITTOLOGFONTSTRUCT
INLINEPREFIX
inlines
Inno
Innolux
INPC
inproc
INPUTHARDWARE
@@ -881,7 +845,6 @@ istep
ith
ITHUMBNAIL
IUI
IVO
IUWP
IWIC
jeli
@@ -895,7 +858,6 @@ jpnime
Jsons
jsonval
jxr
Kantai
keybd
KEYBDDATA
KEYBDINPUT
@@ -917,7 +879,6 @@ KILLFOCUS
killrunner
kmph
kvp
KVM
Kybd
LARGEICON
lastcodeanalysissucceeded
@@ -933,15 +894,12 @@ Lclean
Ldone
Ldr
LEFTALIGN
leftclick
LEFTSCROLLBAR
LEFTTEXT
leftclick
LError
LEVELID
LExit
Lenovo
LGD
LFU
lhwnd
LIBFUZZER
LIBID
@@ -1046,7 +1004,6 @@ MAPTOSAMESHORTCUT
MAPVK
MARKDOWNPREVIEWHANDLERCPP
MAXIMIZEBOX
Maximizebox
MAXSHORTCUTSIZE
maxversiontested
mber
@@ -1059,14 +1016,12 @@ MDL
mdtext
mdtxt
mdwn
mccs
meme
memicmp
MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metacharacter
metadatamatters
Metadatas
metafile
@@ -1081,7 +1036,6 @@ mikeclayton
mindaro
Minimizable
MINIMIZEBOX
Minimizebox
MINIMIZEEND
MINIMIZESTART
MINMAXINFO
@@ -1117,8 +1071,7 @@ mouseutils
MOVESIZEEND
MOVESIZESTART
MRM
Mrt
mrt
MRT
mru
MSAL
msc
@@ -1144,7 +1097,6 @@ Mso
msrc
msstore
mstsc
mswhql
msvcp
MT
MTND
@@ -1162,7 +1114,6 @@ MYICON
myorg
myrepo
NAMECHANGE
Nanjing
namespaceanddescendants
nao
NCACTIVATE
@@ -1231,7 +1182,6 @@ NOMCX
NOMINMAX
NOMIRRORBITMAP
NOMOVE
Nomove
NONANTIALIASED
nonclient
NONCLIENTMETRICSW
@@ -1253,7 +1203,6 @@ NORMALUSER
NOSEARCH
NOSENDCHANGING
NOSIZE
Nosize
NOTHOUSANDS
NOTICKS
NOTIFICATIONSDLL
@@ -1261,11 +1210,9 @@ NOTIFYICONDATA
NOTIFYICONDATAW
NOTIMPL
NOTOPMOST
Notopmost
NOTRACK
NOTSRCCOPY
NOTSRCERASE
Notupdated
notwindows
NOTXORPEN
nowarn
@@ -1309,7 +1256,6 @@ opensource
openurl
openxmlformats
OPTIMIZEFORINVOKE
Optronics
ORPHANEDDIALOGTITLE
ORSCANS
oss
@@ -1345,7 +1291,6 @@ PATINVERT
PATPAINT
pbc
pbi
PBP
PBlob
pbrush
pcb
@@ -1360,7 +1305,6 @@ PDBs
PDEVMODE
pdisp
PDLL
pdmodels
pdo
pdto
pdtobj
@@ -1383,7 +1327,6 @@ pguid
phbm
phbmp
phicon
PHL
Photoshop
phwnd
pici
@@ -1416,8 +1359,6 @@ Popups
POPUPWINDOW
POSITIONITEM
POWERBROADCAST
powerdisplay
POWERDISPLAYMODULEINTERFACE
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1472,7 +1413,6 @@ projectname
PROPERTYKEY
Propset
PROPVARIANT
prot
PRTL
prvpane
psapi
@@ -1500,16 +1440,12 @@ PTOKEN
PToy
ptstr
pui
pvct
PWAs
pwcs
PWSTR
pwsz
pwtd
Qdc
QDC
qdc
QDS
qit
QITAB
QITABENT
@@ -1553,9 +1489,7 @@ regfile
REGISTERCLASSFAILED
REGISTRYHEADER
REGISTRYPREVIEWEXT
registryroot
regkey
regroot
regsvr
REINSTALLMODE
releaseblog
@@ -1600,6 +1534,7 @@ riid
RKey
RNumber
rollups
ROOTOWNER
rop
ROUNDSMALL
ROWSETEXT
@@ -1732,7 +1667,6 @@ sigdn
Signedness
SIGNINGSCENARIO
signtool
SIIGBF
SINGLEKEY
sipolicy
SIZEBOX
@@ -1797,7 +1731,6 @@ STARTUPINFOW
startupscreen
STATFLAG
STATICEDGE
Staticedge
staticmethod
STATSTG
stdafx
@@ -1834,7 +1767,6 @@ subkeys
sublang
SUBMODULEUPDATE
subresource
swp
Superbar
sut
svchost
@@ -1843,8 +1775,7 @@ SVGIO
svgz
SVSI
SWFO
SWP
Swp
swp
SWPNOSIZE
SWPNOZORDER
SWRESTORE
@@ -1904,9 +1835,7 @@ THEMECHANGED
themeresources
THH
THICKFRAME
Thickframe
THISCOMPONENT
Tianma
throughs
TILEDWINDOW
TILLSON
@@ -1987,13 +1916,13 @@ UNLEN
UNORM
unremapped
Unsubscribes
unsubscribes
unvirtualized
unwide
unzoom
UOffset
UOI
UPDATENOW
UPDATEREGISTRY
updown
UPGRADINGPRODUCTCODE
upscaling
@@ -2020,8 +1949,6 @@ vcamp
vcenter
vcgtq
VCINSTALLDIR
vcp
vcpname
Vcpkg
VCRT
vcruntime
@@ -2034,8 +1961,6 @@ VERIFYCONTEXT
VERSIONINFO
VERTRES
VERTSIZE
VESA
vesa
VFT
vget
vgetq
@@ -2067,7 +1992,6 @@ VSM
vso
vsonline
VSpeed
VSync
vstemplate
vstest
VSTHRD
@@ -2109,7 +2033,7 @@ winapi
winappsdk
windir
WINDOWCREATED
windowedge
WINDOWEDGE
WINDOWINFO
WINDOWNAME
WINDOWPLACEMENT
@@ -2133,12 +2057,12 @@ WINL
winlogon
winmd
winml
WINNT
winres
winrt
winsdk
winsta
WINTHRESHOLD
WINNT
WINVER
winxamlmanager
withinrafael
@@ -2150,7 +2074,6 @@ WKSG
Wlkr
wmain
Wman
wmi
WMI
WMICIM
wmimgmt
@@ -2163,7 +2086,6 @@ WNDCLASSEX
WNDCLASSEXW
WNDCLASSW
WNDPROC
Wndproc
wnode
wom
WORKSPACESEDITOR

View File

@@ -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+

View File

@@ -0,0 +1,61 @@
---
description: 'Guidelines for shared libraries including logging, IPC, settings, DPI, telemetry, and utilities consumed by multiple modules'
applyTo: 'src/common/**'
---
# Common Libraries Shared Code Guidance
Guidelines for modifying shared code in `src/common/`. Changes here can have wide-reaching impact across the entire PowerToys codebase.
## Scope
- Logging infrastructure (`src/common/logger/`)
- IPC primitives and named pipe utilities
- Settings serialization and management
- DPI awareness and scaling utilities
- Telemetry helpers
- General utilities (JSON parsing, string helpers, etc.)
## Guidelines
### API Stability
- Avoid breaking public headers/APIs; if changed, search & update all callers
- Coordinate ABI-impacting struct/class layout changes; keep binary compatibility
- When modifying public interfaces, grep the entire codebase for usages
### Performance
- Watch perf in hot paths (hooks, timers, serialization)
- Avoid avoidable allocations in frequently called code
- Profile changes that touch performance-sensitive areas
### Dependencies
- Ask before adding third-party deps or changing serialization formats
- New dependencies must be MIT-licensed or approved by PM team
- Add any new external packages to `NOTICE.md`
### Logging
- C++ logging uses spdlog (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`)
- Initialize with `init_logger()` early in startup
- Keep hot paths quiet no logging in tight loops or hooks
## Acceptance Criteria
- No unintended ABI breaks
- No noisy logs in hot paths
- New non-obvious symbols briefly commented
- All callers updated when interfaces change
## Code Style
- **C++**: Follow `.clang-format` in `src/`; use Modern C++ patterns per C++ Core Guidelines
- **C#**: Follow `src/.editorconfig`; enforce StyleCop.Analyzers
## Validation
- Build: `tools\build\build.cmd` from `src/common/` folder
- Verify no ABI breaks: grep for changed function/struct names across codebase
- Check logs: ensure no new logging in performance-critical paths

View File

@@ -0,0 +1,68 @@
---
description: 'Guidelines for Runner and Settings UI components that communicate via named pipes and manage module lifecycle'
applyTo: 'src/runner/**,src/settings-ui/**'
---
# Runner & Settings UI Core Components Guidance
Guidelines for modifying the Runner (tray/module loader) and Settings UI (configuration app). These components communicate via Windows Named Pipes using JSON messages.
## Runner (`src/runner/`)
### Scope
- Module bootstrap, hotkey management, settings bridge, update/elevation handling
### Guidelines
- If IPC/JSON contracts change, mirror updates in `src/settings-ui/**`
- Keep module discovery in `src/runner/main.cpp` in sync when adding/removing modules
- Keep startup lean: avoid blocking/network calls in early init path
- Preserve GPO & elevation behaviors; confirm no regression in policy handling
- Ask before modifying update workflow or elevation logic
### Acceptance Criteria
- Stable startup, consistent contracts, no unnecessary logging noise
## Settings UI (`src/settings-ui/`)
### Scope
- WinUI/WPF UI, communicates with Runner over named pipes; manages persisted settings schema
### Guidelines
- Don't break settings schema silently; add migration when shape changes
- If IPC/JSON contracts change, align with `src/runner/**` implementation
- Keep UI responsive: marshal to UI thread for UI-bound operations
- Reuse existing styles/resources; avoid duplicate theme keys
- Add/adjust migration or serialization tests when changing persisted settings
### Acceptance Criteria
- Schema integrity preserved, responsive UI, consistent contracts, no style duplication
## Shared Concerns
### IPC Contract Changes
When modifying the JSON message format between Runner and Settings UI:
1. Update both `src/runner/` and `src/settings-ui/` in the same PR
2. Preserve backward compatibility where possible
3. Add migration logic for settings schema changes
4. Test both directions of communication
### Code Style
- **C++ (Runner)**: Follow `.clang-format` in `src/`
- **C# (Settings UI)**: Follow `src/.editorconfig`, use StyleCop.Analyzers
- **XAML**: Use XamlStyler or run `.\.pipelines\applyXamlStyling.ps1 -Main`
## Validation
- Build Runner: `tools\build\build.cmd` from `src/runner/`
- Build Settings UI: `tools\build\build.cmd` from `src/settings-ui/`
- Test IPC: Launch both Runner and Settings UI, verify communication works
- Schema changes: Run serialization tests if settings shape changed

View File

@@ -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"
],

View File

@@ -91,7 +91,6 @@ extends:
official: true
codeSign: true
runTests: false
buildTests: false
signingIdentity:
serviceName: $(SigningServiceName)
appId: $(SigningAppId)

View File

@@ -258,7 +258,6 @@ jobs:
-restore -graph
/p:RestorePackagesConfig=true
/p:CIBuild=true
/p:BuildTests=${{ parameters.buildTests }}
/bl:$(LogOutputDirectory)\build-0-main.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)

View File

@@ -59,7 +59,6 @@ stages:
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
runTests: ${{ parameters.runTests }}
buildTests: true
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
@@ -79,9 +78,7 @@ stages:
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS18-Preview
${{ else }}:
demands: ImageOverride -equals SHINE-VS18-Latest
demands: ImageOverride -equals SHINE-VS17-Preview
buildConfigurations: [Release]
official: false
codeSign: false

View File

@@ -90,15 +90,9 @@ if ($noticeMatch.Success) {
$currentNoticePackageList = ""
}
# Test-only packages that are allowed to be in NOTICE.md but not in the build
# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
$allowedExtraPackages = @(
"- Moq"
)
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
{
Write-Host -ForegroundColor Yellow "Notice.md does not exactly match NuGet list. Analyzing differences..."
Write-Host -ForegroundColor Red "Notice.md does not match NuGet list."
# Show detailed differences
$generatedPackages = $returnList -split "`r`n|`n" | Where-Object { $_.Trim() -ne "" } | Sort-Object
@@ -111,7 +105,7 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
# Find packages in proj file list but not in NOTICE.md
$missingFromNotice = $generatedPackages | Where-Object { $noticePackages -notcontains $_ }
if ($missingFromNotice.Count -gt 0) {
Write-Host -ForegroundColor Red "MissingFromNotice (ERROR - these must be added to NOTICE.md):"
Write-Host -ForegroundColor Red "MissingFromNotice:"
foreach ($pkg in $missingFromNotice) {
Write-Host -ForegroundColor Red " $pkg"
}
@@ -120,23 +114,10 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
# Find packages in NOTICE.md but not in proj file list
$extraInNotice = $noticePackages | Where-Object { $generatedPackages -notcontains $_ }
# Filter out allowed extra packages (test-only dependencies)
$unexpectedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -notcontains $_ }
$allowedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -contains $_ }
if ($allowedExtra.Count -gt 0) {
Write-Host -ForegroundColor Green "ExtraInNotice (OK - allowed test-only packages):"
foreach ($pkg in $allowedExtra) {
Write-Host -ForegroundColor Green " $pkg"
}
Write-Host ""
}
if ($unexpectedExtra.Count -gt 0) {
Write-Host -ForegroundColor Red "ExtraInNotice (ERROR - unexpected packages in NOTICE.md):"
foreach ($pkg in $unexpectedExtra) {
Write-Host -ForegroundColor Red " $pkg"
if ($extraInNotice.Count -gt 0) {
Write-Host -ForegroundColor Yellow "ExtraInNotice:"
foreach ($pkg in $extraInNotice) {
Write-Host -ForegroundColor Yellow " $pkg"
}
Write-Host ""
}
@@ -146,17 +127,10 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
Write-Host " Proj file list has $($generatedPackages.Count) packages"
Write-Host " NOTICE.md has $($noticePackages.Count) packages"
Write-Host " MissingFromNotice: $($missingFromNotice.Count) packages"
Write-Host " ExtraInNotice (allowed): $($allowedExtra.Count) packages"
Write-Host " ExtraInNotice (unexpected): $($unexpectedExtra.Count) packages"
Write-Host " ExtraInNotice: $($extraInNotice.Count) packages"
Write-Host ""
# Fail if there are missing packages OR unexpected extra packages
if ($missingFromNotice.Count -gt 0 -or $unexpectedExtra.Count -gt 0) {
Write-Host -ForegroundColor Red "FAILED: NOTICE.md mismatch detected."
exit 1
} else {
Write-Host -ForegroundColor Green "PASSED: NOTICE.md matches (with allowed test-only packages)."
}
exit 1
}
exit 0

View File

@@ -2,12 +2,6 @@
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Skip building C++ test projects when BuildTests=false -->
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>
<RunCodeAnalysis>false</RunCodeAnalysis>
</PropertyGroup>
<!-- Project configurations -->
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">

View File

@@ -19,39 +19,6 @@
<PlatformTarget>$(Platform)</PlatformTarget>
</PropertyGroup>
<!--
Completely skip building test projects when BuildTests=false (e.g., Release pipeline).
This avoids InternalsVisibleTo/signing issues by not compiling test code at all.
Match: projects ending in Test, Tests, UnitTests, UITests, FuzzTests, or in a folder named Tests.
Also matches projects starting with UnitTests- (e.g., UnitTests-CommonLib).
Also removes all PackageReference/ProjectReference to prevent NuGet restore and dependency builds.
Note: Checking both 'false' and 'False' to handle YAML boolean serialization.
-->
<PropertyGroup Condition="'$(BuildTests)' == 'false' or '$(BuildTests)' == 'False'">
<_ProjectName>$(MSBuildProjectName)</_ProjectName>
<!-- Match any project ending with "Test" or "Tests" (covers UnitTests, UITests, FuzzTests, etc.) -->
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Test'))">true</_IsSkippedTestProject>
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Tests'))">true</_IsSkippedTestProject>
<!-- Match projects starting with UnitTests- or UITest- prefix -->
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UnitTests-'))">true</_IsSkippedTestProject>
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UITest-'))">true</_IsSkippedTestProject>
<!-- Match projects in a Tests folder -->
<_IsSkippedTestProject Condition="$(MSBuildProjectDirectory.Contains('\Tests\'))">true</_IsSkippedTestProject>
</PropertyGroup>
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<EnableDefaultItems>false</EnableDefaultItems>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateGlobalUsings>false</GenerateGlobalUsings>
<ImplicitUsings>disable</ImplicitUsings>
<!-- Disable all code analysis for skipped test projects -->
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<RunAnalyzers>false</RunAnalyzers>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<Version>$(Version).0</Version>
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
@@ -63,9 +30,7 @@
<_PropertySheetDisplayName>PowerToys.Root.Props</_PropertySheetDisplayName>
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
</PropertyGroup>
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -28,41 +28,4 @@
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
</PropertyGroup>
<!-- Skipped test projects when BuildTests=false: no-op build and remove references.
This must be in targets (not props) so it runs AFTER the project file adds its items. -->
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<BuildDependsOn />
<CoreBuildDependsOn />
<RebuildDependsOn />
</PropertyGroup>
<!-- For C# projects: remove all items -->
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.csproj'">
<PackageReference Remove="@(PackageReference)" />
<ProjectReference Remove="@(ProjectReference)" />
<Reference Remove="@(Reference)" />
<Compile Remove="@(Compile)" />
<Content Remove="@(Content)" />
<EmbeddedResource Remove="@(EmbeddedResource)" />
<None Remove="@(None)" />
<Using Remove="@(Using)" />
<GlobalUsing Remove="@(GlobalUsing)" />
</ItemGroup>
<!-- For C++ projects (vcxproj): remove all compile/link items to prevent build -->
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.vcxproj'">
<ClCompile Remove="@(ClCompile)" />
<ClInclude Remove="@(ClInclude)" />
<Link Remove="@(Link)" />
<Lib Remove="@(Lib)" />
<ProjectReference Remove="@(ProjectReference)" />
<None Remove="@(None)" />
<ResourceCompile Remove="@(ResourceCompile)" />
<Midl Remove="@(Midl)" />
</ItemGroup>
<!-- Note: For C++ skipped test projects, build is effectively skipped by removing all compile items above.
We don't define empty Build/Rebuild/Clean targets here because MSBuild Target definitions with Condition
on the Target element still override the default targets even when condition is false. -->
</Project>
</Project>

View File

@@ -63,7 +63,7 @@
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.66.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3405.78" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.10" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
@@ -77,8 +77,10 @@
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.0-experimental4" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="2.0.130-experimental" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.251104000" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.39" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.251106002" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
@@ -91,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" />
@@ -103,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" />
@@ -133,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" />

View File

@@ -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

View File

@@ -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" />
@@ -1059,16 +1041,6 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/tools/SettingsSearchEvaluation/">
<Project Path="tools/SettingsSearchEvaluation/SettingsSearchEvaluation.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="tools/SettingsSearchEvaluation.Tests/SettingsSearchEvaluation.Tests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/Solution Items/">
<File Path=".vsconfig" />
<File Path="Cpp.Build.props" />

View File

@@ -1,244 +0,0 @@
# Windows App SDK Semantic Search API 总结
## 1. 环境与依赖
| 项目 | 版本/值 |
|------|---------|
| **Windows App SDK** | `2.0.0-experimental3` |
| **.NET** | `net9.0-windows10.0.26100.0` |
| **AI Search NuGet** | `Microsoft.WindowsAppSDK.AI` (2.0.57-experimental) |
| **命名空间** | `Microsoft.Windows.AI.Search.Experimental.AppContentIndex` |
| **应用类型** | WinUI 3 MSIX 打包应用 |
---
## 2. 核心 API
### 2.1 索引管理
```csharp
// 创建/打开索引
var result = AppContentIndexer.GetOrCreateIndex("indexName");
if (result.Succeeded) {
_indexer = result.Indexer;
// result.Status: CreatedNew | OpenedExisting
}
// 等待索引能力就绪
await _indexer.WaitForIndexCapabilitiesAsync();
// 等待索引空闲(建索引完成)
await _indexer.WaitForIndexingIdleAsync(TimeSpan.FromSeconds(120));
// 清理
_indexer.RemoveAll(); // 删除所有索引
_indexer.Remove(id); // 删除单个
_indexer.Dispose();
```
### 2.2 添加内容到索引
```csharp
// 索引文本 → 自动建立 TextLexical + TextSemantic 索引
IndexableAppContent textContent = AppManagedIndexableAppContent.CreateFromString(id, text);
_indexer.AddOrUpdate(textContent);
// 索引图片 → 自动建立 ImageSemantic + ImageOcr 索引
IndexableAppContent imageContent = AppManagedIndexableAppContent.CreateFromBitmap(id, softwareBitmap);
_indexer.AddOrUpdate(imageContent);
```
### 2.3 查询
```csharp
// 文本查询
TextQueryOptions options = new TextQueryOptions {
Language = "en-US", // 可选
MatchScope = QueryMatchScope.Unconstrained, // 匹配范围
TextMatchType = TextLexicalMatchType.Fuzzy // Fuzzy | Exact
};
AppIndexTextQuery query = _indexer.CreateTextQuery(searchText, options);
IReadOnlyList<TextQueryMatch> matches = query.GetNextMatches(5);
// 图片查询
ImageQueryOptions imgOptions = new ImageQueryOptions {
MatchScope = QueryMatchScope.Unconstrained,
ImageOcrTextMatchType = TextLexicalMatchType.Fuzzy
};
AppIndexImageQuery imgQuery = _indexer.CreateImageQuery(searchText, imgOptions);
IReadOnlyList<ImageQueryMatch> imgMatches = imgQuery.GetNextMatches(5);
```
### 2.4 能力检查(只读)
```csharp
IndexCapabilities capabilities = _indexer.GetIndexCapabilities();
bool textLexicalOK = capabilities.GetCapabilityState(IndexCapability.TextLexical)
.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
bool textSemanticOK = capabilities.GetCapabilityState(IndexCapability.TextSemantic)
.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
bool imageSemanticOK = capabilities.GetCapabilityState(IndexCapability.ImageSemantic)
.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
bool imageOcrOK = capabilities.GetCapabilityState(IndexCapability.ImageOcr)
.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
```
---
## 3. 四种索引能力
| 能力 | 说明 | 触发方式 |
|------|------|----------|
| `TextLexical` | 词法/关键词搜索 | CreateFromString() 自动 |
| `TextSemantic` | AI 语义搜索 (Embedding) | CreateFromString() 自动 |
| `ImageSemantic` | 图像语义搜索 | CreateFromBitmap() 自动 |
| `ImageOcr` | 图片 OCR 文字搜索 | CreateFromBitmap() 自动 |
---
## 4. 可控选项(有限)
### TextQueryOptions
| 属性 | 类型 | 说明 |
|------|------|------|
| `Language` | string | 查询语言(可选,如 "en-US"|
| `MatchScope` | QueryMatchScope | Unconstrained / Region / ContentItem |
| `TextMatchType` | TextLexicalMatchType | **Fuzzy** / Exact仅影响 Lexical|
### ImageQueryOptions
| 属性 | 类型 | 说明 |
|------|------|------|
| `MatchScope` | QueryMatchScope | Unconstrained / Region / ContentItem |
| `ImageOcrTextMatchType` | TextLexicalMatchType | **Fuzzy** / Exact仅影响 OCR|
### 枚举值说明
**QueryMatchScope:**
- `Unconstrained` - 无约束,同时使用 Lexical + Semantic
- `Region` - 限制在特定区域
- `ContentItem` - 限制在单个内容项
**TextLexicalMatchType:**
- `Fuzzy` - 模糊匹配,允许拼写错误、近似词
- `Exact` - 精确匹配,必须完全一致
---
## 5. 关键限制 ⚠️
| 限制 | 说明 |
|------|------|
| **不能单独指定 Semantic/Lexical** | 系统自动同时使用所有可用能力 |
| **Fuzzy/Exact 只影响 Lexical** | 对 Semantic 搜索无效 |
| **能力检查是只读的** | `GetIndexCapabilities()` 只能查看,不能控制 |
| **无相似度阈值** | 不能设置 Semantic 匹配的阈值 |
| **无结果排序控制** | 无法指定按相关度或其他方式排序 |
| **语言需手动传** | 不会自动检测,需开发者指定 |
| **无相关度分数** | 查询结果不返回匹配分数 |
---
## 6. 典型使用流程
```
┌─────────────────────────────────────────────────────────┐
│ App 启动时 │
├─────────────────────────────────────────────────────────┤
│ 1. GetOrCreateIndex("name") // 创建/打开索引 │
│ 2. WaitForIndexCapabilitiesAsync() // 等待能力就绪 │
│ 3. GetIndexCapabilities() // 检查可用能力 │
│ 4. IndexAll() // 索引所有数据 │
│ ├─ CreateFromString() × N │
│ └─ CreateFromBitmap() × N │
│ 5. WaitForIndexingIdleAsync() // 等待索引完成 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 运行时查询 │
├─────────────────────────────────────────────────────────┤
│ 1. CreateTextQuery(text, options) // 创建查询 │
│ 2. query.GetNextMatches(N) // 获取结果 │
│ 3. 处理 TextQueryMatch / ImageQueryMatch │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ App 退出时 │
├─────────────────────────────────────────────────────────┤
│ 1. _indexer.RemoveAll() // 清理索引 │
│ 2. _indexer.Dispose() // 释放资源 │
└─────────────────────────────────────────────────────────┘
```
---
## 7. 查询结果处理
```csharp
// 文本查询结果
foreach (var match in textMatches)
{
if (match.ContentKind == QueryMatchContentKind.AppManagedText)
{
AppManagedTextQueryMatch textResult = (AppManagedTextQueryMatch)match;
string contentId = match.ContentId; // 内容 ID
int offset = textResult.TextOffset; // 匹配文本偏移
int length = textResult.TextLength; // 匹配文本长度
}
}
// 图片查询结果
foreach (var match in imageMatches)
{
if (match.ContentKind == QueryMatchContentKind.AppManagedImage)
{
AppManagedImageQueryMatch imageResult = (AppManagedImageQueryMatch)match;
string contentId = imageResult.ContentId; // 图片 ID
}
}
```
---
## 8. 能力变化监听
```csharp
// 监听索引能力变化
_indexer.Listener.IndexCapabilitiesChanged += (indexer, capabilities) =>
{
// 重新检查能力状态,更新 UI
LoadAppIndexCapabilities();
};
```
---
## 9. 结论
这是一个**高度封装的黑盒 API**
### 优点 ✅
- 简单易用,几行代码即可实现搜索
- 自动处理 Lexical + Semantic
- 支持文本和图片多模态搜索
- 系统级集成,无需额外部署模型
### 缺点 ❌
- 无法精细控制搜索类型
- 不能只用 Semantic Search
- 选项有限,缺乏高级配置
- 实验性 API可能变更
### 替代方案
**如果需要纯 Semantic Search向量搜索**,建议:
- 直接使用 Embedding 模型生成向量
- 配合向量数据库Azure Cosmos DB、FAISS、Qdrant 等)
---
## 10. 相关 NuGet 包
```xml
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.0-experimental3" />
<PackageReference Include="Microsoft.WindowsAppSDK.AI" Version="2.0.57-experimental" />
```
---
*文档生成日期: 2026-01-21*

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -68,7 +68,6 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set.
- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date.
- If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item.
- **Before opening a PR, ensure your changes build successfully locally and functionality tests pass.** This is especially important for AI-assisted (vibe coding) contributions—always verify AI-generated code works as intended. Exploratory PRs or draft PRs for discussion are exceptions.
- When opening a PR, follow the PR template.
- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge.
- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it.

View File

@@ -1,325 +0,0 @@
# Sparse Package + WinUI 3 调查报告
## 背景
PowerToys 希望使用 Windows App SDK 的 Semantic Search API (`AppContentIndexer.GetOrCreateIndex`) 来实现语义搜索功能。该 API 要求应用具有 **Package Identity**
## 问题现象
### 1. Semantic Search API 调用失败
在 [SemanticSearchIndex.cs](../../src/common/Common.Search/SemanticSearch/SemanticSearchIndex.cs) 中调用 `AppContentIndexer.GetOrCreateIndex(_indexName)` 时,抛出 COM 异常:
```
System.Runtime.InteropServices.COMException (0x80004005): Error HRESULT E_FAIL has been returned from a call to a COM component.
at WinRT.ExceptionHelpers.<ThrowExceptionForHR>g__Throw|39_0(Int32 hr)
at Microsoft.Windows.AI.Search.AppContentIndexer.GetOrCreateIndex(String indexName)
```
### 2. API 要求
根据 [Windows App SDK 文档](https://learn.microsoft.com/en-us/windows/ai/apis/content-search)Semantic Search API 需要:
- Windows 11 24H2 或更高版本
- NPU 硬件支持
- **Package Identity**(应用需要有 MSIX 包标识)
## Sparse Package 方案
### 什么是 Sparse Package
Sparse Package稀疏包是一种为非打包unpackagedWin32 应用提供 Package Identity 的技术,无需完整的 MSIX 打包。
参考:[Grant package identity by packaging with external location](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps)
### 实现架构
```
PowerToysSparse.msix (仅包含 manifest 和图标)
├── AppxManifest.xml (声明应用和依赖)
├── Square44x44Logo.png
├── Square150x150Logo.png
└── StoreLogo.png
ExternalLocation (指向实际应用目录)
└── ARM64\Debug\
├── PowerToys.Settings.exe
├── PowerToys.Settings.pri
└── ... (其他应用文件)
```
### 关键组件
| 文件 | 位置 | 作用 |
|------|------|------|
| AppxManifest.xml | src/PackageIdentity/ | 定义 sparse package 的应用、依赖和能力 |
| app.manifest | src/settings-ui/Settings.UI/ | 嵌入 exe 中,声明与 sparse package 的关联 |
| BuildSparsePackage.ps1 | src/PackageIdentity/ | 构建和签名脚本 |
### Publisher 配置
**重要**app.manifest 和 AppxManifest.xml 中的 Publisher 必须匹配。
| 环境 | Publisher |
|------|-----------|
| 开发环境 | `CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US` |
| 生产环境 | `CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US` |
BuildSparsePackage.ps1 会在本地构建时**自动**将 AppxManifest.xml 的 Publisher 替换为开发环境值,无需手动修改源码。
## 当前问题WinUI 3 + Sparse Package 崩溃
### 现象
当 Settings.exeWinUI 3 应用)通过 sparse package 启动时,立即崩溃:
```
Microsoft.UI.Xaml.Markup.XamlParseException (-2144665590):
Cannot locate resource from 'ms-appx:///Microsoft.UI.Xaml/Themes/themeresources.xaml'. [Line: 11 Position: 40]
```
### 新观察2026-01-25
对齐 WinAppSDK 版本并恢复 app-local 运行时后,仍可复现**更早期**的崩溃(未写入 Settings 日志):
- Application Error / WERAUMID 启动):
- Faulting module: `CoreMessagingXP.dll`
- Exception code: `0xc0000602`
- Faulting module path: `C:\PowerToys\ARM64\Debug\WinUI3Apps\CoreMessagingXP.dll`
- 暂时移除 `CoreMessagingXP.dll` 后,出现 .NET Runtime 1026
- `COMException (0x80040111): ClassFactory cannot supply requested class`
- 发生在 `Microsoft.UI.Xaml.Application.Start(...)`
这说明 **“themeresources.xaml 无法解析”并不是唯一/必现的失败模式**app-local 运行时在 sparse identity 下可能存在更底层的初始化问题。
### 新观察2026-01-25 晚间)
framework-dependent + bootstrap 方向有实质进展:
- 设置 `WindowsAppSDKSelfContained=false`(仅在 `UseSparseIdentity=true` 时生效)
- 添加 `WindowsAppSDKBootstrapAutoInitializeOptions_OnPackageIdentity_NoOp=true`
- **从 ExternalLocation 根目录与 `WinUI3Apps` 目录移除 app-local WinAppSDK 运行时文件**
- 尤其是 `CoreMessagingXP.dll`,否则会优先加载并导致 `0xc0000602`
- **保留/放回 bootstrap DLL**
- 必需:`Microsoft.WindowsAppRuntime.Bootstrap.Net.dll`
- 建议同时保留:`Microsoft.WindowsAppRuntime.Bootstrap.dll`
按以上处理后Settings 通过 AUMID 启动不再崩溃,日志写入恢复。
### 根本原因分析
1. **ms-appx:/// URI 机制**
- WinUI 3 使用 `ms-appx:///` URI 加载 XAML 资源
- 这个 URI scheme 依赖于 MSIX 包的资源索引系统
2. **框架资源位置**
- `themeresources.xaml` 等主题资源在 Windows App Runtime 框架包中
- 框架包位置:`C:\Program Files\WindowsApps\Microsoft.WindowsAppRuntime.2.0-experimental4_*\`(应与 WinAppSDK 版本匹配)
- 资源编译在框架包的 `resources.pri`
3. **WinAppSDK 版本/依赖不一致(更可能的原因)**
- 仓库当前引用 `Microsoft.WindowsAppSDK` **2.0.0-experimental4**(见 `Directory.Packages.props`
- Sparse manifest 仍依赖 **Microsoft.WindowsAppRuntime.2.0-experimental3**`MinVersion=0.676.658.0`
- 通过包标识启动时会走框架包资源图如果依赖版本不匹配WinUI 资源解析可能失败,从而触发上述 `XamlParseException`
- 需要先对齐依赖版本,再判断是否是 sparse 本身限制
4. **app-local 运行时在 sparse identity 下崩溃(已观测)**
- 即使对齐 WinAppSDK 版本,也可能在 `CoreMessagingXP.dll` 处崩溃(`0xc0000602`
- 此时 Settings 日志不一定写入,需查看 Application Event Log
4. **Sparse Package 的限制(待验证)**
- 之前推断 `ms-appx:///` 在 sparse package 下无法解析框架依赖资源
- 但在修正依赖版本之前无法下结论
### 对比WPF 应用可以工作
WPF 应用(如 ImageResizer使用 sparse package 时**可以正常工作**,因为:
- WPF 不依赖 `ms-appx:///` URI
- WPF 资源加载使用不同的机制
## 已尝试的解决方案
| 方案 | 结果 | 原因 |
|------|------|------|
| 复制 PRI 文件到根目录 | ❌ 失败 | `ms-appx:///` 不查找本地 PRI |
| 复制 themeresources 到本地 | ❌ 失败 | 资源在 PRI 中,不是独立文件 |
| 修改 Settings OutputPath 到根目录 | ❌ 失败 | 问题不在于应用资源位置 |
| 复制框架 resources.pri | ❌ 失败 | `ms-appx:///` 机制问题 |
| 对齐 WindowsAppRuntime 依赖版本 | ⏳ 待验证 | 先排除依赖不一致导致的资源解析失败 |
| app-local 运行时self-contained+ sparse identity | ❌ 失败 | Application Error: `CoreMessagingXP.dll` / `0xc0000602` |
| 移除 `CoreMessagingXP.dll` | ❌ 失败 | .NET Runtime 1026: `ClassFactory cannot supply requested class` |
| framework-dependent + bootstrap + 清理 ExternalLocation 中 app-local 运行时 | ✅ 成功 | 需保留 `Microsoft.WindowsAppRuntime.Bootstrap*.dll` |
| 将 resources.pri 打进 sparse MSIX | ✅ 成功 | MRT 可从包内 resources.pri 正常解析字符串 |
## 当前代码状态
### 已修正(建议保留)
1. **Settings.UI 输出路径**
- 文件:`src/settings-ui/Settings.UI/PowerToys.Settings.csproj`
- 修改:恢复为 `WinUI3Apps`(避免破坏 runner/installer/脚本路径假设)
2. **AppxManifest.xml 的 Executable 路径**
- 文件:`src/PackageIdentity/AppxManifest.xml`
- 修改:恢复为 `WinUI3Apps\PowerToys.Settings.exe`
3. **AppxManifest.xml 的 WindowsAppRuntime 依赖**
- 文件:`src/PackageIdentity/AppxManifest.xml`
- 修改:更新为 `Microsoft.WindowsAppRuntime.2.0-experimental4``MinVersion=0.738.2207.0`(与 `Microsoft.WindowsAppSDK.Runtime` 2.0.0-experimental4 对齐)
### 未修改(源码中保持生产配置)
- AppxManifest.xml 的 Publisher 保持 Microsoft Corporation脚本会自动替换
### 验证步骤(建议)
1. **确认 WindowsAppRuntime 版本已安装**
- `Get-AppxPackage -Name Microsoft.WindowsAppRuntime.2.0-experimental4`
- 如缺失,可从 NuGet 缓存安装:
`Add-AppxPackage -Path "$env:USERPROFILE\.nuget\packages\microsoft.windowsappsdk.runtime\2.0.0-experimental4\tools\MSIX\win10-x64\Microsoft.WindowsAppRuntime.2.0-experimental4.msix"`
2. **构建并注册 sparse package**
- `.\src\PackageIdentity\BuildSparsePackage.ps1 -Platform x64 -Configuration Debug`
- `Add-AppxPackage -Path ".\x64\Debug\PowerToysSparse.msix" -ExternalLocation ".\x64\Debug"`
3. **用包标识启动 Settings**
- AUMID`Microsoft.PowerToys.SparseApp!PowerToys.SettingsUI`
- 预期:不再触发 `themeresources.xaml` 解析错误
## 可能的解决方向
### 方向 1等待 Windows App SDK 修复
- 可能是 Windows App SDK 的已知限制或 bug
- 需要在 GitHub issues 中搜索或提交新 issue
### 方向 2使用完整 MSIX 打包
- 不使用 sparse package而是完整打包
- 影响:改变部署模型,增加复杂性
### 方向 3创建非 WinUI 3 的 Helper 进程
- 创建一个 Console App 或 WPF App 作为 helper
- 该 helper 具有 package identity专门调用 Semantic Search API
- Settings 通过 IPC 与 helper 通信
- 优点:不影响现有 Settings 架构
### 方向 4进一步调查 ms-appx:/// 解析
- 研究是否有配置选项让 sparse package 正确解析框架资源
- 可能需要深入 Windows App SDK 源码或联系微软
### 方向 5切换为 framework-dependent + Bootstrap待验证
- 设置 `WindowsAppSDKSelfContained=false` 并**重新构建** Settings
- 确保外部目录不再携带 app-local WinAppSDK 运行时
-`Bootstrap.TryInitialize(...)` 生效,走框架包动态依赖
## 可复现的工作流(已验证 2026-01-25
目标Settings 使用 sparse identity 启动WinUI 资源/字符串正常加载。
### 1) 构建 Settingsframework-dependent + bootstrap no-op
`PowerToys.Settings.csproj` 中添加(仅在 `UseSparseIdentity=true` 时生效):
```
<PropertyGroup Condition="'$(UseSparseIdentity)'=='true'">
<WindowsAppSDKSelfContained>false</WindowsAppSDKSelfContained>
<WindowsAppSDKBootstrapAutoInitializeOptions_OnPackageIdentity_NoOp>true</WindowsAppSDKBootstrapAutoInitializeOptions_OnPackageIdentity_NoOp>
</PropertyGroup>
```
构建:
```
MSBuild.exe src\settings-ui\Settings.UI\PowerToys.Settings.csproj /p:Platform=ARM64 /p:Configuration=Debug /p:UseSparseIdentity=true /m:1 /p:CL_MPCount=1 /nodeReuse:false
```
### 2) 清理 ExternalLocation 的 app-local WinAppSDK 运行时
**必须移除** app-local WinAppSDK 运行时文件,否则会优先加载并崩溃(`CoreMessagingXP.dll` / `0xc0000602`)。
需清理的目录:
- `ARM64\Debug`ExternalLocation 根)
- `ARM64\Debug\WinUI3Apps`
建议只移除 app-local WinAppSDK 相关文件(保留业务 DLL
**保留/放回 bootstrap DLL必要**
- `Microsoft.WindowsAppRuntime.Bootstrap.dll`
- `Microsoft.WindowsAppRuntime.Bootstrap.Net.dll`
### 3) 生成与包名一致的 resources.pri
关键点resources.pri 的 **ResourceMap name 必须与包名一致**
使用 `makepri.exe new` 生成,确保 `/mn` 指向 sparse 包的 `AppxManifest.xml`
```
makepri.exe new ^
/pr C:\PowerToys\src\settings-ui\Settings.UI ^
/cf C:\PowerToys\src\settings-ui\Settings.UI\obj\ARM64\Debug\priconfig.xml ^
/mn C:\PowerToys\src\PackageIdentity\AppxManifest.xml ^
/of C:\PowerToys\ARM64\Debug\resources.pri ^
/o
```
### 4) 将 resources.pri 打进 sparse MSIX
`BuildSparsePackage.ps1` 中把 `resources.pri` 放入 staging脚本已更新
- 优先取 `ARM64\Debug\resources.pri`
- 如果不存在则回退 `ARM64\Debug\WinUI3Apps\PowerToys.Settings.pri`
重新打包:
```
.\src\PackageIdentity\BuildSparsePackage.ps1 -Platform ARM64 -Configuration Debug
```
### 5) 重新注册 sparse 包(如需先卸载)
如果因为内容变更被阻止,先卸载再安装:
```
Get-AppxPackage -Name Microsoft.PowerToys.SparseApp | Remove-AppxPackage
Add-AppxPackage -Path .\ARM64\Debug\PowerToysSparse.msix -ExternalLocation .\ARM64\Debug -ForceApplicationShutdown
```
### 6) 启动 Settings验证
```
Start-Process "shell:AppsFolder\Microsoft.PowerToys.SparseApp_djwsxzxb4ksa8!PowerToys.SettingsUI"
```
验证要点:
- Settings 正常启动UI 文本显示
- 日志正常写入:`%LOCALAPPDATA%\Microsoft\PowerToys\Settings\Logs\0.0.1.0\`
### 备注(可选)
如果出现 `ms-appx:///CommunityToolkit...` 资源缺失,可将对应的 `.pri`(从 NuGet 缓存)复制到 `ARM64\Debug\WinUI3Apps`,但在 **resources.pri 已正确打包** 后通常不再需要。
## 待确认事项
1. [ ] WinUI 3 + Sparse Package 的兼容性问题是否有官方文档说明?
2. [ ] 是否有其他项目成功实现 WinUI 3 + Sparse Package
3. [ ] Windows App SDK GitHub 上是否有相关 issue
4. [ ] 修正依赖版本后Settings 是否能在 sparse identity 下正常启动?
5. [ ] framework-dependentBootstrap方式是否能在 sparse identity 下启动?
## 相关文件
- [SemanticSearchIndex.cs](../../src/common/Common.Search/SemanticSearch/SemanticSearchIndex.cs) - Semantic Search 实现
- [AppxManifest.xml](../../src/PackageIdentity/AppxManifest.xml) - Sparse package manifest
- [BuildSparsePackage.ps1](../../src/PackageIdentity/BuildSparsePackage.ps1) - 构建脚本
- [app.manifest](../../src/settings-ui/Settings.UI/app.manifest) - Settings 应用 manifest
- [PowerToys.Settings.csproj](../../src/settings-ui/Settings.UI/PowerToys.Settings.csproj) - Settings 项目文件
## 参考链接
- [Grant package identity by packaging with external location](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps)
- [Windows App SDK - Content Search API](https://learn.microsoft.com/en-us/windows/ai/apis/content-search)
- [Windows App SDK GitHub Issues](https://github.com/microsoft/WindowsAppSDK/issues)

View File

@@ -1,496 +0,0 @@
# Common.Search Library Specification
## Overview
本文档描述 `Common.Search` 库的重构设计目标是提供一个通用的、可插拔的搜索框架支持多种搜索引擎实现Fuzzy Match、Semantic Search 等)。
## Goals
1. **解耦** - 搜索引擎与数据源完全解耦
2. **可插拔** - 支持替换不同的搜索引擎实现
3. **泛型** - 不绑定特定业务类型(如 SettingEntry
4. **可组合** - 支持多引擎组合(即时 Fuzzy + 延迟 Semantic
5. **可复用** - 可被 PowerToys 多个模块使用
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Consumer (e.g., Settings.UI) │
├─────────────────────────────────────────────────────────────────┤
│ SettingsDataProvider ← 业务特定的数据加载 │
│ SettingsSearchService ← 业务特定的搜索服务 │
│ SettingEntry : ISearchable ← 业务实体实现搜索契约 │
└─────────────────────────────────────────────────────────────────┘
│ uses
┌─────────────────────────────────────────────────────────────────┐
│ Common.Search (Library) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Core Abstractions │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ ISearchable ← 可搜索内容契约 │ │
│ │ ISearchEngine<T> ← 搜索引擎接口 │ │
│ │ SearchResult<T> ← 统一结果模型 │ │
│ │ SearchOptions ← 搜索选项 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Implementations │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ FuzzSearch/ │ │
│ │ ├── FuzzSearchEngine<T> ← 内存 Fuzzy 搜索 │ │
│ │ ├── StringMatcher ← 现有的模糊匹配算法 │ │
│ │ └── MatchResult ← Fuzzy 匹配结果 │ │
│ │ │ │
│ │ SemanticSearch/ │ │
│ │ ├── SemanticSearchEngine ← Windows AI Search 封装 │ │
│ │ └── SemanticSearchCapabilities │ │
│ │ │ │
│ │ CompositeSearch/ │ │
│ │ └── CompositeSearchEngine<T> ← 多引擎组合 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Core Interfaces
### ISearchable
定义可搜索内容的最小契约。
```csharp
namespace Common.Search;
/// <summary>
/// Defines a searchable item that can be indexed and searched.
/// </summary>
public interface ISearchable
{
/// <summary>
/// Gets the unique identifier for this item.
/// </summary>
string Id { get; }
/// <summary>
/// Gets the primary searchable text (e.g., title, header).
/// </summary>
string SearchableText { get; }
/// <summary>
/// Gets optional secondary searchable text (e.g., description).
/// Returns null if not available.
/// </summary>
string? SecondarySearchableText { get; }
}
```
### ISearchEngine&lt;T&gt;
搜索引擎核心接口。
```csharp
namespace Common.Search;
/// <summary>
/// Defines a pluggable search engine that can index and search items.
/// </summary>
/// <typeparam name="T">The type of items to search, must implement ISearchable.</typeparam>
public interface ISearchEngine<T> : IDisposable
where T : ISearchable
{
/// <summary>
/// Gets a value indicating whether the engine is ready to search.
/// </summary>
bool IsReady { get; }
/// <summary>
/// Gets the engine capabilities.
/// </summary>
SearchEngineCapabilities Capabilities { get; }
/// <summary>
/// Initializes the search engine.
/// </summary>
Task InitializeAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Indexes a single item.
/// </summary>
Task IndexAsync(T item, CancellationToken cancellationToken = default);
/// <summary>
/// Indexes multiple items in batch.
/// </summary>
Task IndexBatchAsync(IEnumerable<T> items, CancellationToken cancellationToken = default);
/// <summary>
/// Removes an item from the index by its ID.
/// </summary>
Task RemoveAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Clears all indexed items.
/// </summary>
Task ClearAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Searches for items matching the query.
/// </summary>
Task<IReadOnlyList<SearchResult<T>>> SearchAsync(
string query,
SearchOptions? options = null,
CancellationToken cancellationToken = default);
}
```
### SearchResult&lt;T&gt;
统一的搜索结果模型。
```csharp
namespace Common.Search;
/// <summary>
/// Represents a search result with the matched item and scoring information.
/// </summary>
public sealed class SearchResult<T>
where T : ISearchable
{
/// <summary>
/// Gets the matched item.
/// </summary>
public required T Item { get; init; }
/// <summary>
/// Gets the relevance score (higher is more relevant).
/// </summary>
public required double Score { get; init; }
/// <summary>
/// Gets the type of match that produced this result.
/// </summary>
public required SearchMatchKind MatchKind { get; init; }
/// <summary>
/// Gets the match details for highlighting (optional).
/// </summary>
public IReadOnlyList<MatchSpan>? MatchSpans { get; init; }
}
/// <summary>
/// Represents a span of matched text for highlighting.
/// </summary>
public readonly record struct MatchSpan(int Start, int Length);
/// <summary>
/// Specifies the kind of match that produced a search result.
/// </summary>
public enum SearchMatchKind
{
/// <summary>Exact text match.</summary>
Exact,
/// <summary>Fuzzy/approximate text match.</summary>
Fuzzy,
/// <summary>Semantic/AI-based match.</summary>
Semantic,
/// <summary>Combined match from multiple engines.</summary>
Composite,
}
```
### SearchOptions
搜索配置选项。
```csharp
namespace Common.Search;
/// <summary>
/// Options for configuring search behavior.
/// </summary>
public sealed class SearchOptions
{
/// <summary>
/// Gets or sets the maximum number of results to return.
/// Default is 20.
/// </summary>
public int MaxResults { get; set; } = 20;
/// <summary>
/// Gets or sets the minimum score threshold (0.0 to 1.0).
/// Results below this score are filtered out.
/// Default is 0.0 (no filtering).
/// </summary>
public double MinScore { get; set; } = 0.0;
/// <summary>
/// Gets or sets the language hint for the search (e.g., "en-US").
/// </summary>
public string? Language { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to include match spans for highlighting.
/// Default is false.
/// </summary>
public bool IncludeMatchSpans { get; set; } = false;
}
```
### SearchEngineCapabilities
引擎能力描述。
```csharp
namespace Common.Search;
/// <summary>
/// Describes the capabilities of a search engine.
/// </summary>
public sealed class SearchEngineCapabilities
{
/// <summary>
/// Gets a value indicating whether the engine supports fuzzy matching.
/// </summary>
public bool SupportsFuzzyMatch { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports semantic search.
/// </summary>
public bool SupportsSemanticSearch { get; init; }
/// <summary>
/// Gets a value indicating whether the engine persists the index to disk.
/// </summary>
public bool PersistsIndex { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports incremental indexing.
/// </summary>
public bool SupportsIncrementalIndex { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports match span highlighting.
/// </summary>
public bool SupportsMatchSpans { get; init; }
}
```
## Implementations
### FuzzSearchEngine&lt;T&gt;
基于现有 StringMatcher 的内存搜索引擎。
**特点:**
- 纯内存,无持久化
- 即时响应(毫秒级)
- 支持 match spans 高亮
- 基于字符的模糊匹配
**Capabilities**
```csharp
new SearchEngineCapabilities
{
SupportsFuzzyMatch = true,
SupportsSemanticSearch = false,
PersistsIndex = false,
SupportsIncrementalIndex = true,
SupportsMatchSpans = true,
}
```
### SemanticSearchEngine
基于 Windows App SDK AI Search API 的语义搜索引擎。
**特点:**
- 系统管理的持久化索引
- AI 驱动的语义理解
- 需要模型初始化(可能较慢)
- 可能不可用(依赖系统支持)
**Capabilities**
```csharp
new SearchEngineCapabilities
{
SupportsFuzzyMatch = true, // API 同时提供 lexical + semantic
SupportsSemanticSearch = true,
PersistsIndex = true,
SupportsIncrementalIndex = true,
SupportsMatchSpans = false, // API 不返回详细位置
}
```
**注意:** SemanticSearchEngine 不是泛型的,因为它需要将内容转换为字符串存入系统索引。实现时通过 `ISearchable` 接口提取文本。
### CompositeSearchEngine&lt;T&gt;
组合多个搜索引擎,支持 fallback 和结果合并。
```csharp
namespace Common.Search;
/// <summary>
/// A search engine that combines results from multiple engines.
/// </summary>
public sealed class CompositeSearchEngine<T> : ISearchEngine<T>
where T : ISearchable
{
/// <summary>
/// Strategy for combining results from multiple engines.
/// </summary>
public enum CombineStrategy
{
/// <summary>Use first ready engine only.</summary>
FirstReady,
/// <summary>Merge results from all ready engines.</summary>
MergeAll,
/// <summary>Use primary, fallback to secondary if primary not ready.</summary>
PrimaryWithFallback,
}
}
```
**典型用法:** Fuzzy 作为即时响应Semantic 准备好后增强结果。
## Directory Structure
```
src/common/Common.Search/
├── Common.Search.csproj
├── GlobalSuppressions.cs
├── ISearchable.cs
├── ISearchEngine.cs
├── SearchResult.cs
├── SearchOptions.cs
├── SearchEngineCapabilities.cs
├── SearchMatchKind.cs
├── MatchSpan.cs
├── FuzzSearch/
│ ├── FuzzSearchEngine.cs
│ ├── StringMatcher.cs (existing)
│ ├── MatchOption.cs (existing)
│ ├── MatchResult.cs (existing)
│ └── SearchPrecisionScore.cs (existing)
├── SemanticSearch/
│ ├── SemanticSearchEngine.cs
│ ├── SemanticSearchCapabilities.cs
│ └── SemanticSearchAdapter.cs (adapts ISearchable to Windows API)
└── CompositeSearch/
└── CompositeSearchEngine.cs
```
## Consumer Usage (Settings.UI)
### SettingEntry 实现 ISearchable
```csharp
// Settings.UI.Library/SettingEntry.cs
public struct SettingEntry : ISearchable
{
// Existing properties...
// ISearchable implementation
public string Id => ElementUid ?? $"{PageTypeName}|{ElementName}";
public string SearchableText => Header ?? string.Empty;
public string? SecondarySearchableText => Description;
}
```
### SettingsSearchService
```csharp
// Settings.UI/Services/SettingsSearchService.cs
public sealed class SettingsSearchService : IDisposable
{
private readonly ISearchEngine<SettingEntry> _engine;
public SettingsSearchService(ISearchEngine<SettingEntry> engine)
{
_engine = engine;
}
public async Task InitializeAsync(IEnumerable<SettingEntry> entries)
{
await _engine.InitializeAsync();
await _engine.IndexBatchAsync(entries);
}
public async Task<List<SettingEntry>> SearchAsync(string query, CancellationToken ct = default)
{
var results = await _engine.SearchAsync(query, cancellationToken: ct);
return results.Select(r => r.Item).ToList();
}
}
```
### Startup Configuration
```csharp
// Option 1: Fuzzy only (default, immediate)
var engine = new FuzzSearchEngine<SettingEntry>();
// Option 2: Semantic only (requires Windows AI)
var engine = new SemanticSearchAdapter<SettingEntry>("PowerToysSettings");
// Option 3: Composite (best of both worlds)
var engine = new CompositeSearchEngine<SettingEntry>(
primary: new SemanticSearchAdapter<SettingEntry>("PowerToysSettings"),
fallback: new FuzzSearchEngine<SettingEntry>(),
strategy: CombineStrategy.PrimaryWithFallback
);
var searchService = new SettingsSearchService(engine);
await searchService.InitializeAsync(settingEntries);
```
## Migration Plan
### Phase 1: Core Abstractions
1. 创建 `ISearchable`, `ISearchEngine<T>`, `SearchResult<T>` 等核心接口
2. 保持现有 FuzzSearch 代码不变
### Phase 2: FuzzSearchEngine&lt;T&gt;
1. 创建泛型 `FuzzSearchEngine<T>` 实现
2. 内部复用现有 `StringMatcher`
### Phase 3: SemanticSearchEngine
1. 完善现有 `SemanticSearchEngine` 实现
2. 创建 `SemanticSearchAdapter<T>` 桥接泛型接口
### Phase 4: Settings.UI Migration
1. `SettingEntry` 实现 `ISearchable`
2. 创建 `SettingsSearchService`
3. 迁移 `SearchIndexService` 到新架构
4. 保持 API 兼容,逐步废弃旧方法
### Phase 5: CompositeSearchEngine (Optional)
1. 实现组合引擎
2. 支持 Fuzzy + Semantic 混合搜索
## Open Questions
1. **是否需要支持图片搜索?** 当前 SemanticSearchEngine 支持 `IndexImage`,但 `ISearchable` 只有文本。如果需要图片,可能需要 `IImageSearchable` 扩展。
2. **结果去重策略?** CompositeEngine 合并结果时,同一个 Item 可能被多个引擎匹配,如何去重和合并分数?
3. **异步 vs 同步?** FuzzSearch 完全可以同步执行,但接口统一用 `Task` 是否合适?考虑提供同步重载?
4. **索引更新策略?** 当 Settings 内容变化时(例如用户切换语言),如何高效更新索引?
---
*Document Version: 1.0*
*Last Updated: 2026-01-21*

View File

@@ -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",

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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=&quot;ALL&quot;)" />
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<!-- TODO: Use to activate embedded MSIX -->
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">

View File

@@ -367,12 +367,6 @@
</RegistryKey>
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
</Component>
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)23">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
</Component>
<?undef IdSafeLanguage?>
<?undef CompGUIDPrefix?>
<?endforeach?>

View File

@@ -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

View File

@@ -29,7 +29,6 @@
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.26226.0" />
<PackageDependency Name="Microsoft.WindowsAppRuntime.2.0-experimental4" MinVersion="0.738.2207.0" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" />
</Dependencies>
<Capabilities>
<Capability Name="internetClient" />
@@ -81,7 +80,6 @@
<Extensions>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Microsoft.CmdPal.Ext.PowerToys.exe" Arguments="-RegisterProcessAsComServer" DisplayName="PowerToys Command Palette Extension">
<com:Class Id="7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17" DisplayName="PowerToys Command Palette Extension" />
</com:ExeServer>

View File

@@ -320,19 +320,6 @@ try {
}
}
}
# Include resources.pri in the sparse MSIX if available to enable MRT in packaged mode.
# Prefer a prebuilt resources.pri in output root; fall back to Settings pri.
$resourcesPriSource = Join-Path $outDir "resources.pri"
if (-not (Test-Path $resourcesPriSource)) {
$resourcesPriSource = Join-Path $outDir "WinUI3Apps\\PowerToys.Settings.pri"
}
if (Test-Path $resourcesPriSource) {
Copy-Item -Path $resourcesPriSource -Destination (Join-Path $stagingDir "resources.pri") -Force -ErrorAction SilentlyContinue
Write-BuildLog "Including resources.pri from: $resourcesPriSource" -Level Info
} else {
Write-BuildLog "resources.pri not found; strings may be missing in sparse identity." -Level Warning
}
# Ensure publisher matches the dev certificate for local builds
$manifestStagingPath = Join-Path $stagingDir 'AppxManifest.xml'

View File

@@ -4,16 +4,5 @@
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8305</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ManagedCommon\ManagedCommon.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,306 +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.Globalization;
using System.Text;
namespace Common.Search.FuzzSearch;
/// <summary>
/// A search engine that uses fuzzy string matching for search.
/// </summary>
/// <typeparam name="T">The type of items to search.</typeparam>
public sealed class FuzzSearchEngine<T> : ISearchEngine<T>
where T : ISearchable
{
private readonly object _lockObject = new();
private readonly Dictionary<string, T> _itemsById = new();
private readonly Dictionary<string, (string PrimaryNorm, string? SecondaryNorm)> _normalizedCache = new();
private bool _isReady;
private bool _disposed;
/// <inheritdoc/>
public bool IsReady
{
get
{
lock (_lockObject)
{
return _isReady;
}
}
}
/// <inheritdoc/>
public SearchEngineCapabilities Capabilities { get; } = new()
{
SupportsFuzzyMatch = true,
SupportsSemanticSearch = false,
PersistsIndex = false,
SupportsIncrementalIndex = true,
SupportsMatchSpans = true,
};
/// <inheritdoc/>
public Task InitializeAsync(CancellationToken cancellationToken = default)
{
lock (_lockObject)
{
_isReady = true;
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task IndexAsync(T item, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(item);
ThrowIfDisposed();
lock (_lockObject)
{
_itemsById[item.Id] = item;
_normalizedCache[item.Id] = (
NormalizeString(item.SearchableText),
item.SecondarySearchableText != null ? NormalizeString(item.SecondarySearchableText) : null
);
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task IndexBatchAsync(IEnumerable<T> items, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(items);
ThrowIfDisposed();
lock (_lockObject)
{
foreach (var item in items)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
_itemsById[item.Id] = item;
_normalizedCache[item.Id] = (
NormalizeString(item.SearchableText),
item.SecondarySearchableText != null ? NormalizeString(item.SecondarySearchableText) : null
);
}
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task RemoveAsync(string id, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ThrowIfDisposed();
lock (_lockObject)
{
_itemsById.Remove(id);
_normalizedCache.Remove(id);
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task ClearAsync(CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
lock (_lockObject)
{
_itemsById.Clear();
_normalizedCache.Clear();
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task<IReadOnlyList<SearchResult<T>>> SearchAsync(
string query,
SearchOptions? options = null,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(query))
{
return Task.FromResult<IReadOnlyList<SearchResult<T>>>(Array.Empty<SearchResult<T>>());
}
options ??= new SearchOptions();
var normalizedQuery = NormalizeString(query);
List<KeyValuePair<string, T>> snapshot;
lock (_lockObject)
{
if (_itemsById.Count == 0)
{
return Task.FromResult<IReadOnlyList<SearchResult<T>>>(Array.Empty<SearchResult<T>>());
}
snapshot = _itemsById.ToList();
}
var bag = new ConcurrentBag<SearchResult<T>>();
var po = new ParallelOptions
{
CancellationToken = cancellationToken,
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount - 1),
};
try
{
Parallel.ForEach(snapshot, po, kvp =>
{
var (primaryNorm, secondaryNorm) = GetNormalizedTexts(kvp.Key);
var primaryResult = StringMatcher.FuzzyMatch(normalizedQuery, primaryNorm);
double score = primaryResult.Score;
List<int>? matchData = primaryResult.MatchData;
if (!string.IsNullOrEmpty(secondaryNorm))
{
var secondaryResult = StringMatcher.FuzzyMatch(normalizedQuery, secondaryNorm);
if (secondaryResult.Success && secondaryResult.Score * 0.8 > score)
{
score = secondaryResult.Score * 0.8;
matchData = null; // Secondary matches don't have primary text spans
}
}
if (score > options.MinScore)
{
var result = new SearchResult<T>
{
Item = kvp.Value,
Score = score,
MatchKind = SearchMatchKind.Fuzzy,
MatchSpans = options.IncludeMatchSpans && matchData != null
? ConvertToMatchSpans(matchData)
: null,
};
bag.Add(result);
}
});
}
catch (OperationCanceledException)
{
return Task.FromResult<IReadOnlyList<SearchResult<T>>>(Array.Empty<SearchResult<T>>());
}
var results = bag
.OrderByDescending(r => r.Score)
.Take(options.MaxResults)
.ToList();
return Task.FromResult<IReadOnlyList<SearchResult<T>>>(results);
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed)
{
return;
}
lock (_lockObject)
{
_itemsById.Clear();
_normalizedCache.Clear();
_isReady = false;
}
_disposed = true;
}
private (string PrimaryNorm, string? SecondaryNorm) GetNormalizedTexts(string id)
{
lock (_lockObject)
{
if (_normalizedCache.TryGetValue(id, out var cached))
{
return cached;
}
}
return (string.Empty, null);
}
private static IReadOnlyList<MatchSpan> ConvertToMatchSpans(List<int> matchData)
{
if (matchData == null || matchData.Count == 0)
{
return Array.Empty<MatchSpan>();
}
// Convert individual match indices to spans
var spans = new List<MatchSpan>();
var sortedIndices = matchData.OrderBy(i => i).ToList();
int start = sortedIndices[0];
int length = 1;
for (int i = 1; i < sortedIndices.Count; i++)
{
if (sortedIndices[i] == sortedIndices[i - 1] + 1)
{
// Consecutive index, extend the span
length++;
}
else
{
// Gap found, save current span and start new one
spans.Add(new MatchSpan(start, length));
start = sortedIndices[i];
length = 1;
}
}
// Add the last span
spans.Add(new MatchSpan(start, length));
return spans;
}
private static string NormalizeString(string? input)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
var normalized = input.ToLowerInvariant().Normalize(NormalizationForm.FormKD);
var sb = new StringBuilder(normalized.Length);
foreach (var c in normalized)
{
var category = CharUnicodeInfo.GetUnicodeCategory(c);
if (category != UnicodeCategory.NonSpacingMark)
{
sb.Append(c);
}
}
return sb.ToString();
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
}

View File

@@ -25,9 +25,9 @@ public class StringMatcher
/// 6. Move onto the next substring's characters until all substrings are checked.
/// 7. Consider success and move onto scoring if every char or substring without whitespaces matched
/// </summary>
public static MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption? opt = null)
public static MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt = null)
{
opt ??= new MatchOption();
opt = opt ?? new MatchOption();
if (string.IsNullOrEmpty(stringToCompare))
{

View File

@@ -11,10 +11,6 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.MatchResult._rawScore")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._defaultMatchOption")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._instance")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.SemanticSearch.SemanticSearchIndex._indexName")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.SemanticSearch.SemanticSearchIndex._indexer")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.SemanticSearch.SemanticSearchIndex._disposed")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.SemanticSearch.SemanticSearchIndex._capabilities")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.MatchResult.#ctor(System.Boolean,Common.Search.SearchPrecisionScore)")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.MatchResult.#ctor(System.Boolean,Common.Search.SearchPrecisionScore,System.Collections.Generic.List{System.Int32},System.Int32)")]
[assembly: SuppressMessage("Compiler", "CS8618:Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.", Justification = "Coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._instance")]

View File

@@ -1,73 +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 Common.Search;
/// <summary>
/// Defines a pluggable search engine that can index and search items.
/// </summary>
/// <typeparam name="T">The type of items to search, must implement ISearchable.</typeparam>
public interface ISearchEngine<T> : IDisposable
where T : ISearchable
{
/// <summary>
/// Gets a value indicating whether the engine is ready to search.
/// </summary>
bool IsReady { get; }
/// <summary>
/// Gets the engine capabilities.
/// </summary>
SearchEngineCapabilities Capabilities { get; }
/// <summary>
/// Initializes the search engine.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the async operation.</returns>
Task InitializeAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Indexes a single item.
/// </summary>
/// <param name="item">The item to index.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the async operation.</returns>
Task IndexAsync(T item, CancellationToken cancellationToken = default);
/// <summary>
/// Indexes multiple items in batch.
/// </summary>
/// <param name="items">The items to index.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the async operation.</returns>
Task IndexBatchAsync(IEnumerable<T> items, CancellationToken cancellationToken = default);
/// <summary>
/// Removes an item from the index by its ID.
/// </summary>
/// <param name="id">The ID of the item to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the async operation.</returns>
Task RemoveAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Clears all indexed items.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the async operation.</returns>
Task ClearAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Searches for items matching the query.
/// </summary>
/// <param name="query">The search query.</param>
/// <param name="options">Optional search options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list of search results ordered by relevance.</returns>
Task<IReadOnlyList<SearchResult<T>>> SearchAsync(
string query,
SearchOptions? options = null,
CancellationToken cancellationToken = default);
}

View File

@@ -1,27 +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 Common.Search;
/// <summary>
/// Defines a searchable item that can be indexed and searched.
/// </summary>
public interface ISearchable
{
/// <summary>
/// Gets the unique identifier for this item.
/// </summary>
string Id { get; }
/// <summary>
/// Gets the primary searchable text (e.g., title, header).
/// </summary>
string SearchableText { get; }
/// <summary>
/// Gets optional secondary searchable text (e.g., description).
/// Returns null if not available.
/// </summary>
string? SecondarySearchableText { get; }
}

View File

@@ -1,12 +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 Common.Search;
/// <summary>
/// Represents a span of matched text for highlighting.
/// </summary>
/// <param name="Start">The starting index of the match.</param>
/// <param name="Length">The length of the match.</param>
public readonly record struct MatchSpan(int Start, int Length);

View File

@@ -1,36 +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 Common.Search;
/// <summary>
/// Describes the capabilities of a search engine.
/// </summary>
public sealed class SearchEngineCapabilities
{
/// <summary>
/// Gets a value indicating whether the engine supports fuzzy matching.
/// </summary>
public bool SupportsFuzzyMatch { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports semantic search.
/// </summary>
public bool SupportsSemanticSearch { get; init; }
/// <summary>
/// Gets a value indicating whether the engine persists the index to disk.
/// </summary>
public bool PersistsIndex { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports incremental indexing.
/// </summary>
public bool SupportsIncrementalIndex { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports match span highlighting.
/// </summary>
public bool SupportsMatchSpans { get; init; }
}

View File

@@ -1,134 +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 Common.Search;
/// <summary>
/// Represents an error that occurred during a search operation.
/// </summary>
public sealed class SearchError
{
/// <summary>
/// Initializes a new instance of the <see cref="SearchError"/> class.
/// </summary>
/// <param name="code">The error code.</param>
/// <param name="message">The error message.</param>
/// <param name="details">Optional additional details.</param>
/// <param name="exception">Optional exception that caused the error.</param>
public SearchError(SearchErrorCode code, string message, string? details = null, Exception? exception = null)
{
Code = code;
Message = message;
Details = details;
Exception = exception;
Timestamp = DateTime.UtcNow;
}
/// <summary>
/// Gets the error code.
/// </summary>
public SearchErrorCode Code { get; }
/// <summary>
/// Gets the error message.
/// </summary>
public string Message { get; }
/// <summary>
/// Gets additional details about the error.
/// </summary>
public string? Details { get; }
/// <summary>
/// Gets the exception that caused the error, if any.
/// </summary>
public Exception? Exception { get; }
/// <summary>
/// Gets the timestamp when the error occurred.
/// </summary>
public DateTime Timestamp { get; }
/// <summary>
/// Creates an error for initialization failure.
/// </summary>
/// <param name="indexName">The name of the index.</param>
/// <param name="details">Optional details.</param>
/// <param name="exception">Optional exception.</param>
/// <returns>A new SearchError instance.</returns>
public static SearchError InitializationFailed(string indexName, string? details = null, Exception? exception = null)
=> new(SearchErrorCode.InitializationFailed, $"Failed to initialize search index '{indexName}'.", details, exception);
/// <summary>
/// Creates an error for indexing failure.
/// </summary>
/// <param name="contentId">The ID of the content that failed to index.</param>
/// <param name="details">Optional details.</param>
/// <param name="exception">Optional exception.</param>
/// <returns>A new SearchError instance.</returns>
public static SearchError IndexingFailed(string contentId, string? details = null, Exception? exception = null)
=> new(SearchErrorCode.IndexingFailed, $"Failed to index content '{contentId}'.", details, exception);
/// <summary>
/// Creates an error for search query failure.
/// </summary>
/// <param name="query">The search query that failed.</param>
/// <param name="details">Optional details.</param>
/// <param name="exception">Optional exception.</param>
/// <returns>A new SearchError instance.</returns>
public static SearchError SearchFailed(string query, string? details = null, Exception? exception = null)
=> new(SearchErrorCode.SearchFailed, $"Search query '{query}' failed.", details, exception);
/// <summary>
/// Creates an error for engine not ready.
/// </summary>
/// <param name="operation">The operation that was attempted.</param>
/// <returns>A new SearchError instance.</returns>
public static SearchError EngineNotReady(string operation)
=> new(SearchErrorCode.EngineNotReady, $"Search engine is not ready. Operation '{operation}' cannot be performed.");
/// <summary>
/// Creates an error for capability unavailable.
/// </summary>
/// <param name="capability">The capability that is unavailable.</param>
/// <param name="details">Optional details.</param>
/// <returns>A new SearchError instance.</returns>
public static SearchError CapabilityUnavailable(string capability, string? details = null)
=> new(SearchErrorCode.CapabilityUnavailable, $"Search capability '{capability}' is not available.", details);
/// <summary>
/// Creates an error for timeout.
/// </summary>
/// <param name="operation">The operation that timed out.</param>
/// <param name="timeout">The timeout duration.</param>
/// <returns>A new SearchError instance.</returns>
public static SearchError Timeout(string operation, TimeSpan timeout)
=> new(SearchErrorCode.Timeout, $"Operation '{operation}' timed out after {timeout.TotalSeconds:F1} seconds.");
/// <summary>
/// Creates an error for an unexpected error.
/// </summary>
/// <param name="operation">The operation that failed.</param>
/// <param name="exception">The exception that occurred.</param>
/// <returns>A new SearchError instance.</returns>
public static SearchError Unexpected(string operation, Exception exception)
=> new(SearchErrorCode.Unexpected, $"Unexpected error during '{operation}'.", exception.Message, exception);
/// <inheritdoc/>
public override string ToString()
{
var result = $"[{Code}] {Message}";
if (!string.IsNullOrEmpty(Details))
{
result += $" Details: {Details}";
}
if (Exception != null)
{
result += $" Exception: {Exception.GetType().Name}: {Exception.Message}";
}
return result;
}
}

View File

@@ -1,51 +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 Common.Search;
/// <summary>
/// Defines error codes for search operations.
/// </summary>
public enum SearchErrorCode
{
/// <summary>
/// No error occurred.
/// </summary>
None = 0,
/// <summary>
/// The search engine failed to initialize.
/// </summary>
InitializationFailed = 1,
/// <summary>
/// Failed to index content.
/// </summary>
IndexingFailed = 2,
/// <summary>
/// The search query failed to execute.
/// </summary>
SearchFailed = 3,
/// <summary>
/// The search engine is not ready to perform the operation.
/// </summary>
EngineNotReady = 4,
/// <summary>
/// A required capability is not available.
/// </summary>
CapabilityUnavailable = 5,
/// <summary>
/// The operation timed out.
/// </summary>
Timeout = 6,
/// <summary>
/// An unexpected error occurred.
/// </summary>
Unexpected = 99,
}

View File

@@ -1,31 +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 Common.Search;
/// <summary>
/// Specifies the kind of match that produced a search result.
/// </summary>
public enum SearchMatchKind
{
/// <summary>
/// Exact text match.
/// </summary>
Exact,
/// <summary>
/// Fuzzy/approximate text match.
/// </summary>
Fuzzy,
/// <summary>
/// Semantic/AI-based match.
/// </summary>
Semantic,
/// <summary>
/// Combined match from multiple engines.
/// </summary>
Composite,
}

View File

@@ -1,53 +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 Common.Search;
/// <summary>
/// Represents the result of a search operation that may have errors.
/// </summary>
public sealed class SearchOperationResult
{
private SearchOperationResult(bool success, SearchError? error = null)
{
IsSuccess = success;
Error = error;
}
/// <summary>
/// Gets a value indicating whether the operation was successful.
/// </summary>
public bool IsSuccess { get; }
/// <summary>
/// Gets a value indicating whether the operation failed.
/// </summary>
public bool IsFailure => !IsSuccess;
/// <summary>
/// Gets the error if the operation failed, null otherwise.
/// </summary>
public SearchError? Error { get; }
/// <summary>
/// Creates a successful result.
/// </summary>
/// <returns>A successful SearchOperationResult.</returns>
public static SearchOperationResult Success() => new(true);
/// <summary>
/// Creates a failed result with the specified error.
/// </summary>
/// <param name="error">The error that caused the failure.</param>
/// <returns>A failed SearchOperationResult.</returns>
public static SearchOperationResult Failure(SearchError error)
{
ArgumentNullException.ThrowIfNull(error);
return new SearchOperationResult(false, error);
}
/// <inheritdoc/>
public override string ToString()
=> IsSuccess ? "Success" : $"Failure: {Error}";
}

View File

@@ -1,87 +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.Diagnostics.CodeAnalysis;
#pragma warning disable SA1649 // File name should match first type name - Generic type file naming convention
namespace Common.Search;
/// <summary>
/// Represents the result of a search operation that returns a value and may have errors.
/// </summary>
/// <typeparam name="T">The type of the result value.</typeparam>
public sealed class SearchOperationResult<T>
{
private SearchOperationResult(bool success, T? value, SearchError? error)
{
IsSuccess = success;
Value = value;
Error = error;
}
/// <summary>
/// Gets a value indicating whether the operation was successful.
/// </summary>
public bool IsSuccess { get; }
/// <summary>
/// Gets a value indicating whether the operation failed.
/// </summary>
public bool IsFailure => !IsSuccess;
/// <summary>
/// Gets the result value if the operation was successful.
/// </summary>
public T? Value { get; }
/// <summary>
/// Gets the error if the operation failed, null otherwise.
/// </summary>
public SearchError? Error { get; }
/// <summary>
/// Gets the value or a default if the operation failed.
/// </summary>
/// <param name="defaultValue">The default value to return if the operation failed.</param>
/// <returns>The value if successful, otherwise the default value.</returns>
public T GetValueOrDefault(T defaultValue) => IsSuccess && Value is not null ? Value : defaultValue;
/// <inheritdoc/>
public override string ToString()
=> IsSuccess ? $"Success: {Value}" : $"Failure: {Error}";
/// <summary>
/// Creates a successful result with the specified value.
/// </summary>
/// <param name="value">The result value.</param>
/// <returns>A successful SearchOperationResult.</returns>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Factory method pattern is the idiomatic way to create instances of generic result types")]
public static SearchOperationResult<T> Success(T value) => new(true, value, null);
/// <summary>
/// Creates a failed result with the specified error.
/// </summary>
/// <param name="error">The error that caused the failure.</param>
/// <returns>A failed SearchOperationResult.</returns>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Factory method pattern is the idiomatic way to create instances of generic result types")]
public static SearchOperationResult<T> Failure(SearchError error)
{
ArgumentNullException.ThrowIfNull(error);
return new SearchOperationResult<T>(false, default, error);
}
/// <summary>
/// Creates a failed result with the specified error and a fallback value.
/// </summary>
/// <param name="error">The error that caused the failure.</param>
/// <param name="fallbackValue">A fallback value to use despite the failure.</param>
/// <returns>A failed SearchOperationResult with a fallback value.</returns>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Factory method pattern is the idiomatic way to create instances of generic result types")]
public static SearchOperationResult<T> FailureWithFallback(SearchError error, T fallbackValue)
{
ArgumentNullException.ThrowIfNull(error);
return new SearchOperationResult<T>(false, fallbackValue, error);
}
}

View File

@@ -1,35 +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 Common.Search;
/// <summary>
/// Options for configuring search behavior.
/// </summary>
public sealed class SearchOptions
{
/// <summary>
/// Gets or sets the maximum number of results to return.
/// Default is 20.
/// </summary>
public int MaxResults { get; set; } = 20;
/// <summary>
/// Gets or sets the minimum score threshold.
/// Results below this score are filtered out.
/// Default is 0.0 (no filtering).
/// </summary>
public double MinScore { get; set; }
/// <summary>
/// Gets or sets the language hint for the search (e.g., "en-US").
/// </summary>
public string? Language { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to include match spans for highlighting.
/// Default is false.
/// </summary>
public bool IncludeMatchSpans { get; set; }
}

View File

@@ -1,33 +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 Common.Search;
/// <summary>
/// Represents a search result with the matched item and scoring information.
/// </summary>
/// <typeparam name="T">The type of the matched item.</typeparam>
public sealed class SearchResult<T>
where T : ISearchable
{
/// <summary>
/// Gets the matched item.
/// </summary>
public required T Item { get; init; }
/// <summary>
/// Gets the relevance score (higher is more relevant).
/// </summary>
public required double Score { get; init; }
/// <summary>
/// Gets the type of match that produced this result.
/// </summary>
public required SearchMatchKind MatchKind { get; init; }
/// <summary>
/// Gets the match details for highlighting (optional).
/// </summary>
public IReadOnlyList<MatchSpan>? MatchSpans { get; init; }
}

View File

@@ -1,46 +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 Common.Search.SemanticSearch;
/// <summary>
/// Represents the capabilities of the semantic search index.
/// </summary>
public class SemanticSearchCapabilities
{
/// <summary>
/// Gets or sets a value indicating whether text lexical (keyword) search is available.
/// </summary>
public bool TextLexicalAvailable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether text semantic (AI embedding) search is available.
/// </summary>
public bool TextSemanticAvailable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether image semantic search is available.
/// </summary>
public bool ImageSemanticAvailable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether image OCR search is available.
/// </summary>
public bool ImageOcrAvailable { get; set; }
/// <summary>
/// Gets a value indicating whether any search capability is available.
/// </summary>
public bool AnyAvailable => TextLexicalAvailable || TextSemanticAvailable || ImageSemanticAvailable || ImageOcrAvailable;
/// <summary>
/// Gets a value indicating whether text search (lexical or semantic) is available.
/// </summary>
public bool TextSearchAvailable => TextLexicalAvailable || TextSemanticAvailable;
/// <summary>
/// Gets a value indicating whether image search (semantic or OCR) is available.
/// </summary>
public bool ImageSearchAvailable => ImageSemanticAvailable || ImageOcrAvailable;
}

View File

@@ -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.
namespace Common.Search.SemanticSearch;
/// <summary>
/// Specifies the kind of content in a semantic search result.
/// </summary>
public enum SemanticSearchContentKind
{
/// <summary>
/// Text content.
/// </summary>
Text,
/// <summary>
/// Image content.
/// </summary>
Image,
}

View File

@@ -1,406 +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 ManagedCommon;
namespace Common.Search.SemanticSearch;
/// <summary>
/// A semantic search engine that implements the common search interface.
/// </summary>
/// <typeparam name="T">The type of items to search.</typeparam>
public sealed class SemanticSearchEngine<T> : ISearchEngine<T>
where T : ISearchable
{
private readonly SemanticSearchIndex _index;
private readonly Dictionary<string, T> _itemsById = new();
private readonly object _lockObject = new();
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="SemanticSearchEngine{T}"/> class.
/// </summary>
/// <param name="indexName">The name of the search index.</param>
public SemanticSearchEngine(string indexName)
{
Logger.LogDebug($"[SemanticSearchEngine] Creating engine. IndexName={indexName}, ItemType={typeof(T).Name}");
_index = new SemanticSearchIndex(indexName);
}
/// <inheritdoc/>
public bool IsReady => _index.IsInitialized;
/// <inheritdoc/>
public SearchEngineCapabilities Capabilities { get; } = new()
{
SupportsFuzzyMatch = true,
SupportsSemanticSearch = true,
PersistsIndex = true,
SupportsIncrementalIndex = true,
SupportsMatchSpans = false,
};
/// <summary>
/// Gets the underlying semantic search capabilities.
/// </summary>
public SemanticSearchCapabilities? SemanticCapabilities => _index.Capabilities;
/// <summary>
/// Gets the last error that occurred during a search operation, or null if no error occurred.
/// </summary>
public SearchError? LastError => _index.LastError;
/// <summary>
/// Occurs when the semantic search capabilities change.
/// </summary>
public event EventHandler<SemanticSearchCapabilities>? CapabilitiesChanged
{
add => _index.CapabilitiesChanged += value;
remove => _index.CapabilitiesChanged -= value;
}
/// <inheritdoc/>
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
Logger.LogInfo($"[SemanticSearchEngine] InitializeAsync starting. ItemType={typeof(T).Name}");
var result = await _index.InitializeAsync().ConfigureAwait(false);
if (result.IsFailure)
{
Logger.LogWarning($"[SemanticSearchEngine] InitializeAsync failed. ItemType={typeof(T).Name}, Error={result.Error?.Message}");
}
else
{
Logger.LogInfo($"[SemanticSearchEngine] InitializeAsync completed. ItemType={typeof(T).Name}");
}
// Note: We don't throw here to maintain backward compatibility,
// but callers can check LastError for details if initialization failed.
}
/// <summary>
/// Initializes the search engine and returns the result with error details if any.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A result indicating success or failure with error details.</returns>
public async Task<SearchOperationResult> InitializeWithResultAsync(CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
return await _index.InitializeAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public Task IndexAsync(T item, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(item);
ThrowIfDisposed();
var text = BuildSearchableText(item);
if (string.IsNullOrWhiteSpace(text))
{
Logger.LogDebug($"[SemanticSearchEngine] IndexAsync skipped (empty text). Id={item.Id}");
return Task.CompletedTask;
}
lock (_lockObject)
{
_itemsById[item.Id] = item;
}
Logger.LogDebug($"[SemanticSearchEngine] IndexAsync. Id={item.Id}, TextLength={text.Length}");
// Note: Errors are captured in LastError for external logging
_ = _index.IndexText(item.Id, text);
return Task.CompletedTask;
}
/// <summary>
/// Indexes a single item and returns the result with error details if any.
/// </summary>
/// <param name="item">The item to index.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A result indicating success or failure with error details.</returns>
public Task<SearchOperationResult> IndexWithResultAsync(T item, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(item);
ThrowIfDisposed();
var text = BuildSearchableText(item);
if (string.IsNullOrWhiteSpace(text))
{
return Task.FromResult(SearchOperationResult.Success());
}
lock (_lockObject)
{
_itemsById[item.Id] = item;
}
return Task.FromResult(_index.IndexText(item.Id, text));
}
/// <inheritdoc/>
public Task IndexBatchAsync(IEnumerable<T> items, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(items);
ThrowIfDisposed();
var batch = new List<(string Id, string Text)>();
lock (_lockObject)
{
foreach (var item in items)
{
if (cancellationToken.IsCancellationRequested)
{
Logger.LogDebug($"[SemanticSearchEngine] IndexBatchAsync cancelled. ItemsProcessed={batch.Count}");
break;
}
var text = BuildSearchableText(item);
if (!string.IsNullOrWhiteSpace(text))
{
_itemsById[item.Id] = item;
batch.Add((item.Id, text));
}
}
}
Logger.LogInfo($"[SemanticSearchEngine] IndexBatchAsync. BatchSize={batch.Count}");
// Note: Errors are captured in LastError for external logging
_ = _index.IndexTextBatch(batch);
return Task.CompletedTask;
}
/// <summary>
/// Indexes multiple items in batch and returns the result with error details if any.
/// </summary>
/// <param name="items">The items to index.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A result indicating success or failure with error details.</returns>
public Task<SearchOperationResult> IndexBatchWithResultAsync(IEnumerable<T> items, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(items);
ThrowIfDisposed();
var batch = new List<(string Id, string Text)>();
lock (_lockObject)
{
foreach (var item in items)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
var text = BuildSearchableText(item);
if (!string.IsNullOrWhiteSpace(text))
{
_itemsById[item.Id] = item;
batch.Add((item.Id, text));
}
}
}
return Task.FromResult(_index.IndexTextBatch(batch));
}
/// <inheritdoc/>
public Task RemoveAsync(string id, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ThrowIfDisposed();
lock (_lockObject)
{
_itemsById.Remove(id);
}
Logger.LogDebug($"[SemanticSearchEngine] RemoveAsync. Id={id}");
// Note: Errors are captured in LastError for external logging
_ = _index.Remove(id);
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task ClearAsync(CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
int count;
lock (_lockObject)
{
count = _itemsById.Count;
_itemsById.Clear();
}
Logger.LogInfo($"[SemanticSearchEngine] ClearAsync. ItemsCleared={count}");
// Note: Errors are captured in LastError for external logging
_ = _index.RemoveAll();
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task<IReadOnlyList<SearchResult<T>>> SearchAsync(
string query,
SearchOptions? options = null,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(query))
{
Logger.LogDebug($"[SemanticSearchEngine] SearchAsync skipped (empty query).");
return Task.FromResult<IReadOnlyList<SearchResult<T>>>(Array.Empty<SearchResult<T>>());
}
options ??= new SearchOptions();
Logger.LogDebug($"[SemanticSearchEngine] SearchAsync starting. Query={query}, MaxResults={options.MaxResults}");
var semanticOptions = new SemanticSearchOptions
{
MaxResults = options.MaxResults,
Language = options.Language,
MatchScope = SemanticSearchMatchScope.Unconstrained,
TextMatchType = SemanticSearchTextMatchType.Fuzzy,
};
var searchResult = _index.SearchText(query, semanticOptions);
// Note: Errors are captured in LastError for external logging
var matches = searchResult.Value ?? Array.Empty<SemanticSearchResult>();
var results = new List<SearchResult<T>>();
lock (_lockObject)
{
foreach (var match in matches)
{
if (_itemsById.TryGetValue(match.ContentId, out var item))
{
results.Add(new SearchResult<T>
{
Item = item,
Score = 100.0, // Semantic search doesn't return scores, use fixed value
MatchKind = SearchMatchKind.Semantic,
MatchSpans = null,
});
}
}
}
Logger.LogDebug($"[SemanticSearchEngine] SearchAsync completed. Query={query}, Matches={matches.Count}, Results={results.Count}");
return Task.FromResult<IReadOnlyList<SearchResult<T>>>(results);
}
/// <summary>
/// Searches for items matching the query and returns the result with error details if any.
/// </summary>
/// <param name="query">The search query.</param>
/// <param name="options">Optional search options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A result containing search results or error details.</returns>
public Task<SearchOperationResult<IReadOnlyList<SearchResult<T>>>> SearchWithResultAsync(
string query,
SearchOptions? options = null,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(query))
{
return Task.FromResult(SearchOperationResult<IReadOnlyList<SearchResult<T>>>.Success(Array.Empty<SearchResult<T>>()));
}
options ??= new SearchOptions();
var semanticOptions = new SemanticSearchOptions
{
MaxResults = options.MaxResults,
Language = options.Language,
MatchScope = SemanticSearchMatchScope.Unconstrained,
TextMatchType = SemanticSearchTextMatchType.Fuzzy,
};
var searchResult = _index.SearchText(query, semanticOptions);
var matches = searchResult.Value ?? Array.Empty<SemanticSearchResult>();
var results = new List<SearchResult<T>>();
lock (_lockObject)
{
foreach (var match in matches)
{
if (_itemsById.TryGetValue(match.ContentId, out var item))
{
results.Add(new SearchResult<T>
{
Item = item,
Score = 100.0,
MatchKind = SearchMatchKind.Semantic,
MatchSpans = null,
});
}
}
}
if (searchResult.IsFailure)
{
return Task.FromResult(SearchOperationResult<IReadOnlyList<SearchResult<T>>>.FailureWithFallback(searchResult.Error!, results));
}
return Task.FromResult(SearchOperationResult<IReadOnlyList<SearchResult<T>>>.Success(results));
}
/// <summary>
/// Waits for the indexing process to complete.
/// </summary>
/// <param name="timeout">The maximum time to wait.</param>
/// <returns>A task representing the async operation.</returns>
public async Task WaitForIndexingCompleteAsync(TimeSpan timeout)
{
ThrowIfDisposed();
await _index.WaitForIndexingCompleteAsync(timeout).ConfigureAwait(false);
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed)
{
return;
}
Logger.LogDebug($"[SemanticSearchEngine] Disposing. ItemType={typeof(T).Name}");
_index.Dispose();
lock (_lockObject)
{
_itemsById.Clear();
}
_disposed = true;
}
private static string BuildSearchableText(T item)
{
var primary = item.SearchableText ?? string.Empty;
var secondary = item.SecondarySearchableText;
if (string.IsNullOrWhiteSpace(secondary))
{
return primary;
}
return $"{primary} {secondary}";
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
}

View File

@@ -1,455 +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 ManagedCommon;
using Microsoft.Windows.Search.AppContentIndex;
using Windows.Graphics.Imaging;
using SearchOperationResult = Common.Search.SearchOperationResult;
using SearchOperationResultT = Common.Search.SearchOperationResult<System.Collections.Generic.IReadOnlyList<Common.Search.SemanticSearch.SemanticSearchResult>>;
namespace Common.Search.SemanticSearch;
/// <summary>
/// A semantic search engine powered by Windows App SDK AI Search APIs.
/// Provides text and image indexing with lexical and semantic search capabilities.
/// </summary>
public sealed class SemanticSearchIndex : IDisposable
{
private readonly string _indexName;
private AppContentIndexer? _indexer;
private bool _disposed;
private SemanticSearchCapabilities? _capabilities;
/// <summary>
/// Initializes a new instance of the <see cref="SemanticSearchIndex"/> class.
/// </summary>
/// <param name="indexName">The name of the search index.</param>
public SemanticSearchIndex(string indexName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(indexName);
_indexName = indexName;
}
/// <summary>
/// Gets the last error that occurred during an operation, or null if no error occurred.
/// </summary>
public SearchError? LastError { get; private set; }
/// <summary>
/// Occurs when the index capabilities change.
/// </summary>
public event EventHandler<SemanticSearchCapabilities>? CapabilitiesChanged;
/// <summary>
/// Gets a value indicating whether the search engine is initialized.
/// </summary>
public bool IsInitialized => _indexer != null;
/// <summary>
/// Gets the current index capabilities, or null if not initialized.
/// </summary>
public SemanticSearchCapabilities? Capabilities => _capabilities;
/// <summary>
/// Initializes the search engine and creates or opens the index.
/// </summary>
/// <returns>A task that represents the asynchronous operation. Returns a result indicating success or failure with error details.</returns>
public async Task<SearchOperationResult> InitializeAsync()
{
ThrowIfDisposed();
LastError = null;
if (_indexer != null)
{
Logger.LogDebug($"[SemanticSearchIndex] Already initialized. IndexName={_indexName}");
return SearchOperationResult.Success();
}
Logger.LogInfo($"[SemanticSearchIndex] Initializing. IndexName={_indexName}");
try
{
var result = AppContentIndexer.GetOrCreateIndex(_indexName);
if (!result.Succeeded)
{
var errorDetails = $"Succeeded={result.Succeeded}, ExtendedError={result.ExtendedError}";
Logger.LogError($"[SemanticSearchIndex] GetOrCreateIndex failed. IndexName={_indexName}, {errorDetails}");
LastError = SearchError.InitializationFailed(_indexName, errorDetails);
return SearchOperationResult.Failure(LastError);
}
_indexer = result.Indexer;
// Wait for index capabilities to be ready
Logger.LogDebug($"[SemanticSearchIndex] Waiting for index capabilities. IndexName={_indexName}");
await _indexer.WaitForIndexCapabilitiesAsync();
// Load capabilities
_capabilities = LoadCapabilities();
Logger.LogInfo($"[SemanticSearchIndex] Initialized successfully. IndexName={_indexName}, TextLexical={_capabilities.TextLexicalAvailable}, TextSemantic={_capabilities.TextSemanticAvailable}, ImageSemantic={_capabilities.ImageSemanticAvailable}, ImageOcr={_capabilities.ImageOcrAvailable}");
// Subscribe to capability changes
_indexer.Listener.IndexCapabilitiesChanged += OnIndexCapabilitiesChanged;
return SearchOperationResult.Success();
}
catch (Exception ex)
{
Logger.LogError($"[SemanticSearchIndex] Initialization failed with exception. IndexName={_indexName}", ex);
LastError = SearchError.InitializationFailed(_indexName, ex.Message, ex);
return SearchOperationResult.Failure(LastError);
}
}
/// <summary>
/// Waits for the indexing process to complete.
/// </summary>
/// <param name="timeout">The maximum time to wait for indexing to complete.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task WaitForIndexingCompleteAsync(TimeSpan timeout)
{
ThrowIfDisposed();
ThrowIfNotInitialized();
await _indexer!.WaitForIndexingIdleAsync(timeout);
}
/// <summary>
/// Gets the current index capabilities.
/// </summary>
/// <returns>The current capabilities of the search index.</returns>
public SemanticSearchCapabilities GetCapabilities()
{
ThrowIfDisposed();
ThrowIfNotInitialized();
return _capabilities ?? LoadCapabilities();
}
/// <summary>
/// Adds or updates text content in the index.
/// </summary>
/// <param name="id">The unique identifier for the content.</param>
/// <param name="text">The text content to index.</param>
/// <returns>A result indicating success or failure with error details.</returns>
public SearchOperationResult IndexText(string id, string text)
{
ThrowIfDisposed();
ThrowIfNotInitialized();
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ArgumentException.ThrowIfNullOrWhiteSpace(text);
try
{
var content = AppManagedIndexableAppContent.CreateFromString(id, text);
_indexer!.AddOrUpdate(content);
return SearchOperationResult.Success();
}
catch (Exception ex)
{
Logger.LogError($"[SemanticSearchIndex] IndexText failed. Id={id}", ex);
LastError = SearchError.IndexingFailed(id, ex.Message, ex);
return SearchOperationResult.Failure(LastError);
}
}
/// <summary>
/// Adds or updates multiple text contents in the index.
/// </summary>
/// <param name="items">A collection of id-text pairs to index.</param>
/// <returns>A result indicating success or failure with error details. Contains the first error encountered if any.</returns>
public SearchOperationResult IndexTextBatch(IEnumerable<(string Id, string Text)> items)
{
ThrowIfDisposed();
ThrowIfNotInitialized();
ArgumentNullException.ThrowIfNull(items);
SearchError? firstError = null;
foreach (var (id, text) in items)
{
if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(text))
{
try
{
var content = AppManagedIndexableAppContent.CreateFromString(id, text);
_indexer!.AddOrUpdate(content);
}
catch (Exception ex)
{
Logger.LogError($"[SemanticSearchIndex] IndexTextBatch item failed. Id={id}", ex);
firstError ??= SearchError.IndexingFailed(id, ex.Message, ex);
}
}
}
if (firstError != null)
{
LastError = firstError;
return SearchOperationResult.Failure(firstError);
}
return SearchOperationResult.Success();
}
/// <summary>
/// Adds or updates image content in the index.
/// </summary>
/// <param name="id">The unique identifier for the image.</param>
/// <param name="bitmap">The image bitmap to index.</param>
/// <returns>A result indicating success or failure with error details.</returns>
public SearchOperationResult IndexImage(string id, SoftwareBitmap bitmap)
{
ThrowIfDisposed();
ThrowIfNotInitialized();
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ArgumentNullException.ThrowIfNull(bitmap);
try
{
var content = AppManagedIndexableAppContent.CreateFromBitmap(id, bitmap);
_indexer!.AddOrUpdate(content);
return SearchOperationResult.Success();
}
catch (Exception ex)
{
Logger.LogError($"[SemanticSearchIndex] IndexImage failed. Id={id}", ex);
LastError = SearchError.IndexingFailed(id, ex.Message, ex);
return SearchOperationResult.Failure(LastError);
}
}
/// <summary>
/// Removes content from the index by its identifier.
/// </summary>
/// <param name="id">The unique identifier of the content to remove.</param>
/// <returns>A result indicating success or failure with error details.</returns>
public SearchOperationResult Remove(string id)
{
ThrowIfDisposed();
ThrowIfNotInitialized();
ArgumentException.ThrowIfNullOrWhiteSpace(id);
try
{
_indexer!.Remove(id);
return SearchOperationResult.Success();
}
catch (Exception ex)
{
Logger.LogError($"[SemanticSearchIndex] Remove failed. Id={id}", ex);
LastError = SearchError.Unexpected("Remove", ex);
return SearchOperationResult.Failure(LastError);
}
}
/// <summary>
/// Removes all content from the index.
/// </summary>
/// <returns>A result indicating success or failure with error details.</returns>
public SearchOperationResult RemoveAll()
{
ThrowIfDisposed();
ThrowIfNotInitialized();
try
{
_indexer!.RemoveAll();
return SearchOperationResult.Success();
}
catch (Exception ex)
{
Logger.LogError($"[SemanticSearchIndex] RemoveAll failed.", ex);
LastError = SearchError.Unexpected("RemoveAll", ex);
return SearchOperationResult.Failure(LastError);
}
}
/// <summary>
/// Searches for text content in the index.
/// </summary>
/// <param name="searchText">The text to search for.</param>
/// <param name="options">Optional search options.</param>
/// <returns>A result containing search results or error details.</returns>
public SearchOperationResultT SearchText(string searchText, SemanticSearchOptions? options = null)
{
ThrowIfDisposed();
ThrowIfNotInitialized();
ArgumentException.ThrowIfNullOrWhiteSpace(searchText);
options ??= new SemanticSearchOptions();
try
{
var queryOptions = new TextQueryOptions
{
MatchScope = ConvertMatchScope(options.MatchScope),
TextMatchType = ConvertTextMatchType(options.TextMatchType),
};
if (!string.IsNullOrEmpty(options.Language))
{
queryOptions.Language = options.Language;
}
var query = _indexer!.CreateTextQuery(searchText, queryOptions);
var matches = query.GetNextMatches(options.MaxResults);
return SearchOperationResultT.Success(ConvertTextMatches(matches));
}
catch (Exception ex)
{
Logger.LogError($"[SemanticSearchIndex] SearchText failed. Query={searchText}", ex);
LastError = SearchError.SearchFailed(searchText, ex.Message, ex);
return SearchOperationResultT.FailureWithFallback(LastError, Array.Empty<SemanticSearchResult>());
}
}
/// <summary>
/// Searches for image content in the index using text.
/// </summary>
/// <param name="searchText">The text to search for in images.</param>
/// <param name="options">Optional search options.</param>
/// <returns>A result containing search results or error details.</returns>
public SearchOperationResultT SearchImages(string searchText, SemanticSearchOptions? options = null)
{
ThrowIfDisposed();
ThrowIfNotInitialized();
ArgumentException.ThrowIfNullOrWhiteSpace(searchText);
options ??= new SemanticSearchOptions();
try
{
var queryOptions = new ImageQueryOptions
{
MatchScope = ConvertMatchScope(options.MatchScope),
ImageOcrTextMatchType = ConvertTextMatchType(options.TextMatchType),
};
var query = _indexer!.CreateImageQuery(searchText, queryOptions);
var matches = query.GetNextMatches(options.MaxResults);
return SearchOperationResultT.Success(ConvertImageMatches(matches));
}
catch (Exception ex)
{
Logger.LogError($"[SemanticSearchIndex] SearchImages failed. Query={searchText}", ex);
LastError = SearchError.SearchFailed(searchText, ex.Message, ex);
return SearchOperationResultT.FailureWithFallback(LastError, Array.Empty<SemanticSearchResult>());
}
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed)
{
return;
}
if (_indexer != null)
{
_indexer.Listener.IndexCapabilitiesChanged -= OnIndexCapabilitiesChanged;
_indexer.Dispose();
_indexer = null;
}
_disposed = true;
}
private SemanticSearchCapabilities LoadCapabilities()
{
var capabilities = _indexer!.GetIndexCapabilities();
return new SemanticSearchCapabilities
{
TextLexicalAvailable = IsCapabilityInitialized(capabilities, IndexCapability.TextLexical),
TextSemanticAvailable = IsCapabilityInitialized(capabilities, IndexCapability.TextSemantic),
ImageSemanticAvailable = IsCapabilityInitialized(capabilities, IndexCapability.ImageSemantic),
ImageOcrAvailable = IsCapabilityInitialized(capabilities, IndexCapability.ImageOcr),
};
}
private static bool IsCapabilityInitialized(IndexCapabilities capabilities, IndexCapability capability)
{
var state = capabilities.GetCapabilityState(capability);
return state.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
}
private void OnIndexCapabilitiesChanged(AppContentIndexer indexer, IndexCapabilities capabilities)
{
_capabilities = LoadCapabilities();
Logger.LogInfo($"[SemanticSearchIndex] Capabilities changed. IndexName={_indexName}, TextLexical={_capabilities.TextLexicalAvailable}, TextSemantic={_capabilities.TextSemanticAvailable}, ImageSemantic={_capabilities.ImageSemanticAvailable}, ImageOcr={_capabilities.ImageOcrAvailable}");
CapabilitiesChanged?.Invoke(this, _capabilities);
}
private static QueryMatchScope ConvertMatchScope(SemanticSearchMatchScope scope)
{
return scope switch
{
SemanticSearchMatchScope.Unconstrained => QueryMatchScope.Unconstrained,
SemanticSearchMatchScope.Region => QueryMatchScope.Region,
SemanticSearchMatchScope.ContentItem => QueryMatchScope.ContentItem,
_ => QueryMatchScope.Unconstrained,
};
}
private static TextLexicalMatchType ConvertTextMatchType(SemanticSearchTextMatchType matchType)
{
return matchType switch
{
SemanticSearchTextMatchType.Fuzzy => TextLexicalMatchType.Fuzzy,
SemanticSearchTextMatchType.Exact => TextLexicalMatchType.Exact,
_ => TextLexicalMatchType.Fuzzy,
};
}
private static IReadOnlyList<SemanticSearchResult> ConvertTextMatches(IReadOnlyList<TextQueryMatch> matches)
{
var results = new List<SemanticSearchResult>();
foreach (var match in matches)
{
var result = new SemanticSearchResult(match.ContentId, SemanticSearchContentKind.Text);
if (match.ContentKind == QueryMatchContentKind.AppManagedText &&
match is AppManagedTextQueryMatch textMatch)
{
result.TextOffset = textMatch.TextOffset;
result.TextLength = textMatch.TextLength;
}
results.Add(result);
}
return results;
}
private static IReadOnlyList<SemanticSearchResult> ConvertImageMatches(IReadOnlyList<ImageQueryMatch> matches)
{
var results = new List<SemanticSearchResult>();
foreach (var match in matches)
{
var result = new SemanticSearchResult(match.ContentId, SemanticSearchContentKind.Image);
results.Add(result);
}
return results;
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
private void ThrowIfNotInitialized()
{
if (_indexer == null)
{
throw new InvalidOperationException("Search engine is not initialized. Call InitializeAsync() first.");
}
}
}

View File

@@ -1,26 +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 Common.Search.SemanticSearch;
/// <summary>
/// Specifies the scope for semantic search matching.
/// </summary>
public enum SemanticSearchMatchScope
{
/// <summary>
/// No constraints, uses both Lexical and Semantic matching.
/// </summary>
Unconstrained,
/// <summary>
/// Restrict matching to a specific region.
/// </summary>
Region,
/// <summary>
/// Restrict matching to a single content item.
/// </summary>
ContentItem,
}

View File

@@ -1,31 +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 Common.Search.SemanticSearch;
/// <summary>
/// Options for configuring semantic search queries.
/// </summary>
public class SemanticSearchOptions
{
/// <summary>
/// Gets or sets the language for the search query (e.g., "en-US").
/// </summary>
public string? Language { get; set; }
/// <summary>
/// Gets or sets the match scope for the search.
/// </summary>
public SemanticSearchMatchScope MatchScope { get; set; } = SemanticSearchMatchScope.Unconstrained;
/// <summary>
/// Gets or sets the text match type for lexical matching.
/// </summary>
public SemanticSearchTextMatchType TextMatchType { get; set; } = SemanticSearchTextMatchType.Fuzzy;
/// <summary>
/// Gets or sets the maximum number of results to return.
/// </summary>
public int MaxResults { get; set; } = 10;
}

View File

@@ -1,42 +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 Common.Search.SemanticSearch;
/// <summary>
/// Represents a search result from the semantic search engine.
/// </summary>
public class SemanticSearchResult
{
/// <summary>
/// Initializes a new instance of the <see cref="SemanticSearchResult"/> class.
/// </summary>
/// <param name="contentId">The unique identifier of the matched content.</param>
/// <param name="contentKind">The kind of content matched (text or image).</param>
public SemanticSearchResult(string contentId, SemanticSearchContentKind contentKind)
{
ContentId = contentId;
ContentKind = contentKind;
}
/// <summary>
/// Gets the unique identifier of the matched content.
/// </summary>
public string ContentId { get; }
/// <summary>
/// Gets the kind of content that was matched.
/// </summary>
public SemanticSearchContentKind ContentKind { get; }
/// <summary>
/// Gets or sets the text offset where the match was found (for text matches only).
/// </summary>
public int TextOffset { get; set; }
/// <summary>
/// Gets or sets the length of the matched text (for text matches only).
/// </summary>
public int TextLength { get; set; }
}

View File

@@ -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.
namespace Common.Search.SemanticSearch;
/// <summary>
/// Specifies the type of text matching for lexical searches.
/// </summary>
public enum SemanticSearchTextMatchType
{
/// <summary>
/// Fuzzy matching allows spelling errors and approximate words.
/// </summary>
Fuzzy,
/// <summary>
/// Exact matching requires exact text matches.
/// </summary>
Exact,
}

View File

@@ -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;

View File

@@ -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());

View File

@@ -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();

View File

@@ -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();

View File

@@ -30,7 +30,6 @@ namespace ManagedCommon
PowerRename,
PowerLauncher,
PowerAccent,
PowerDisplay,
RegistryPreview,
MeasureTool,
ShortcutGuide,

View File

@@ -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());
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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());
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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));
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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());
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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());
}
};
}

View File

@@ -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")));
}
};
}

View File

@@ -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());
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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());
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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());
}
};
}

View File

@@ -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());
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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;
}
}

View File

@@ -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"";
}
}

View File

@@ -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);
}
};
}

View File

@@ -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");
}
};
}

View File

@@ -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());
}
}
};
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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());
}
};
}

View File

@@ -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);
}
};
}

View File

@@ -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>

View File

@@ -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.

Some files were not shown because too many files have changed in this diff Show More