mirror of
https://github.com/LizardByte/libvirtualhid.git
synced 2026-10-23 03:27:34 +09:00
test: add Pester coverage for Windows scripts (#143)
This commit is contained in:
1 parent
688209a178
commit
53e1a949fc
14 files changed
+1512
-1
No files matched your search
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: CI-PowerShell (called)
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Pester
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Run Pester tests
|
||||
id: test
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path ./build/reports -Force | Out-Null
|
||||
|
||||
Import-Module Pester -RequiredVersion 5.9.0 -Force
|
||||
$config = New-PesterConfiguration
|
||||
$config.Run.Path = "./tests/scripts"
|
||||
$config.Run.Exit = $true
|
||||
$config.Output.Verbosity = "Detailed"
|
||||
$config.TestResult.Enabled = $true
|
||||
$config.TestResult.OutputFormat = "JUnitXml"
|
||||
$config.TestResult.OutputPath = "./build/reports/junit.xml"
|
||||
$config.CodeCoverage.Enabled = $true
|
||||
$config.CodeCoverage.Path = @(
|
||||
Get-ChildItem -Path "./scripts/windows/*.ps1" |
|
||||
Select-Object -ExpandProperty FullName
|
||||
)
|
||||
$config.CodeCoverage.OutputFormat = "Cobertura"
|
||||
$config.CodeCoverage.OutputPath = "./build/reports/coverage.xml"
|
||||
|
||||
Invoke-Pester -Configuration $config
|
||||
|
||||
- name: Upload report artifact
|
||||
if: >-
|
||||
always() &&
|
||||
(steps.test.outcome == 'success' || steps.test.outcome == 'failure')
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: reports-PowerShell
|
||||
path: build/reports
|
||||
if-no-files-found: error
|
||||
@@ -37,6 +37,9 @@ jobs:
|
||||
- build_name: Windows-MSVC
|
||||
flag: Windows-MSVC
|
||||
has_coverage: true
|
||||
- build_name: PowerShell
|
||||
flag: PowerShell
|
||||
has_coverage: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -75,13 +75,22 @@ jobs:
|
||||
release_commit: ${{ needs.setup_release.outputs.release_commit }}
|
||||
release_version: ${{ needs.setup_release.outputs.release_version }}
|
||||
|
||||
powershell:
|
||||
name: PowerShell
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/ci-powershell.yml
|
||||
|
||||
results:
|
||||
name: Coverage and Test Results
|
||||
if: >-
|
||||
always() &&
|
||||
(needs.build.result == 'success' || needs.build.result == 'failure') &&
|
||||
(needs.powershell.result == 'success' || needs.powershell.result == 'failure') &&
|
||||
startsWith(github.repository, 'LizardByte/')
|
||||
needs: build
|
||||
needs:
|
||||
- build
|
||||
- powershell
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/ci-results.yml
|
||||
|
||||
@@ -323,6 +323,10 @@ function Install-RootDeviceWithSetupApi {
|
||||
Invoke-CheckedCommand -FilePath $SetupHelperPath -Arguments @("install", $Path, $TargetHardwareId)
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -eq ".") {
|
||||
return
|
||||
}
|
||||
|
||||
Start-LibVirtualHidTranscript -Path $LogPath
|
||||
|
||||
try {
|
||||
|
||||
@@ -60,6 +60,10 @@ function Invoke-CheckedCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -eq ".") {
|
||||
return
|
||||
}
|
||||
|
||||
$resolvedPackagePath = (Resolve-Path -LiteralPath $PackagePath).Path
|
||||
$catalogPath = Join-Path $resolvedPackagePath $CatalogName
|
||||
if (-not (Test-Path -LiteralPath $catalogPath)) {
|
||||
|
||||
@@ -348,6 +348,10 @@ function Get-GamepadApiProbeExpression {
|
||||
"@
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -eq ".") {
|
||||
return
|
||||
}
|
||||
|
||||
if ($HoldSeconds -le $TimeoutSeconds) {
|
||||
throw "-HoldSeconds must be greater than -TimeoutSeconds so the adapter remains alive for browser polling."
|
||||
}
|
||||
|
||||
@@ -339,6 +339,10 @@ function Invoke-GamepadAdapterSmoke {
|
||||
}
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -eq ".") {
|
||||
return
|
||||
}
|
||||
|
||||
Assert-RootDeviceStarted -TargetHardwareId $HardwareId
|
||||
Assert-ControlDeviceOpen -Path $ControlDevicePath
|
||||
Invoke-GamepadAdapterSmoke `
|
||||
|
||||
@@ -201,6 +201,10 @@ function Remove-DriverCertificate {
|
||||
}
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -eq ".") {
|
||||
return
|
||||
}
|
||||
|
||||
Start-LibVirtualHidTranscript -Path $LogPath
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
BeforeAll {
|
||||
$sourcePath = Join-Path $PSScriptRoot "..\..\scripts\windows\install-driver.ps1"
|
||||
. $sourcePath -InfPath "unused.inf"
|
||||
|
||||
function global:Invoke-LibVirtualHidTestCommand {
|
||||
param([Parameter(ValueFromRemainingArguments)] $Arguments)
|
||||
|
||||
$null = $Arguments
|
||||
$global:LASTEXITCODE = [int] $env:LIBVIRTUALHID_INSTALL_TEST_EXIT_CODE
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Item -LiteralPath Function:\Invoke-LibVirtualHidTestCommand -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\LIBVIRTUALHID_INSTALL_TEST_EXIT_CODE -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Describe "Invoke-CheckedCommand" {
|
||||
It "accepts a configured success exit code" {
|
||||
$env:LIBVIRTUALHID_INSTALL_TEST_EXIT_CODE = 5
|
||||
|
||||
{
|
||||
Invoke-CheckedCommand `
|
||||
-FilePath "Invoke-LibVirtualHidTestCommand" `
|
||||
-Arguments @("one", "two") `
|
||||
-SuccessExitCodes @(0, 5)
|
||||
} | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "throws for an unexpected exit code" {
|
||||
$env:LIBVIRTUALHID_INSTALL_TEST_EXIT_CODE = 12
|
||||
|
||||
{
|
||||
Invoke-CheckedCommand `
|
||||
-FilePath "Invoke-LibVirtualHidTestCommand" `
|
||||
-Arguments @("one")
|
||||
} | Should -Throw "*exited with code 12*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "install-driver.ps1 entry point" {
|
||||
It "executes normal invocation and validates the INF path" {
|
||||
{
|
||||
& $sourcePath -InfPath (Join-Path $TestDrive "missing.inf") -StageOnly
|
||||
} | Should -Throw "*Cannot find path*"
|
||||
}
|
||||
|
||||
It "runs the complete orchestration path safely with WhatIf" {
|
||||
$infPath = Join-Path $TestDrive "libvirtualhid.inf"
|
||||
$setupPath = Join-Path $TestDrive "libvirtualhid_driver_setup.exe"
|
||||
New-Item -ItemType File -Path $infPath, $setupPath | Out-Null
|
||||
function pnputil.exe {
|
||||
$global:LASTEXITCODE = 0
|
||||
@()
|
||||
}
|
||||
Mock Get-CimInstance { @() }
|
||||
Mock Get-ChildItem { @() }
|
||||
|
||||
try {
|
||||
{
|
||||
& $sourcePath `
|
||||
-InfPath $infPath `
|
||||
-SetupPath $setupPath `
|
||||
-HardwareId "ROOT\LIBVIRTUALHID_PESTER" `
|
||||
-WhatIf
|
||||
} | Should -Not -Throw
|
||||
} finally {
|
||||
Remove-Item -LiteralPath Function:\pnputil.exe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Driver package path helpers" {
|
||||
It "resolves an explicitly supplied broker" {
|
||||
$path = Join-Path $TestDrive "libvirtualhid_broker.exe"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
|
||||
Resolve-LibVirtualHidBrokerPath -Path $path | Should -Be (Resolve-Path $path).Path
|
||||
}
|
||||
|
||||
It "rejects a missing explicitly supplied broker" {
|
||||
{
|
||||
Resolve-LibVirtualHidBrokerPath -Path (Join-Path $TestDrive "missing.exe")
|
||||
} | Should -Throw "The broker executable was not found*"
|
||||
}
|
||||
|
||||
It "returns null when no packaged broker exists" {
|
||||
Resolve-LibVirtualHidBrokerPath | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It "resolves an explicitly supplied setup helper" {
|
||||
$path = Join-Path $TestDrive "libvirtualhid_driver_setup.exe"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
|
||||
Resolve-LibVirtualHidDriverSetupPath -Path $path | Should -Be (Resolve-Path $path).Path
|
||||
}
|
||||
|
||||
It "rejects a missing explicitly supplied setup helper" {
|
||||
{
|
||||
Resolve-LibVirtualHidDriverSetupPath -Path (Join-Path $TestDrive "missing.exe")
|
||||
} | Should -Throw "The driver setup helper was not found*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Service path quoting" {
|
||||
It "quotes a broker path" {
|
||||
Get-LibVirtualHidQuotedServiceBinaryPath -Path "C:\Program Files\libvirtualhid\broker.exe" |
|
||||
Should -Be '"C:\Program Files\libvirtualhid\broker.exe"'
|
||||
}
|
||||
|
||||
It "rejects quotation marks in a broker path" {
|
||||
{
|
||||
Get-LibVirtualHidQuotedServiceBinaryPath -Path 'C:\bad"path\broker.exe'
|
||||
} | Should -Throw "*must not contain quotation marks*"
|
||||
}
|
||||
|
||||
It "formats the service path for Windows PowerShell native argument parsing" {
|
||||
Get-LibVirtualHidScBinaryPathArgument -Path "C:\Program Files\libvirtualhid\broker.exe" |
|
||||
Should -Be '"""C:\Program Files\libvirtualhid\broker.exe"""'
|
||||
}
|
||||
|
||||
It "accepts an exactly quoted service ImagePath" {
|
||||
Mock Get-ItemProperty {
|
||||
[pscustomobject]@{ ImagePath = '"C:\Program Files\libvirtualhid\broker.exe"' }
|
||||
}
|
||||
|
||||
{
|
||||
Assert-LibVirtualHidBrokerServiceImagePath `
|
||||
-Name "libvirtualhid_broker" `
|
||||
-Path "C:\Program Files\libvirtualhid\broker.exe"
|
||||
} | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "rejects an unquoted service ImagePath" {
|
||||
Mock Get-ItemProperty {
|
||||
[pscustomobject]@{ ImagePath = "C:\Program Files\libvirtualhid\broker.exe" }
|
||||
}
|
||||
|
||||
{
|
||||
Assert-LibVirtualHidBrokerServiceImagePath `
|
||||
-Name "libvirtualhid_broker" `
|
||||
-Path "C:\Program Files\libvirtualhid\broker.exe"
|
||||
} | Should -Throw "*is not safely quoted*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Broker service helpers" {
|
||||
BeforeEach {
|
||||
Mock Remove-ItemProperty {}
|
||||
Mock Stop-Service {}
|
||||
}
|
||||
|
||||
It "does not clear a missing service registry key" {
|
||||
Mock Test-Path { $false }
|
||||
|
||||
Clear-LibVirtualHidBrokerServiceEnvironment -Name "libvirtualhid_broker"
|
||||
|
||||
Should -Invoke Remove-ItemProperty -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "clears a legacy service environment" {
|
||||
Mock Test-Path { $true }
|
||||
|
||||
Clear-LibVirtualHidBrokerServiceEnvironment -Name "libvirtualhid_broker" -Confirm:$false
|
||||
|
||||
Should -Invoke Remove-ItemProperty -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$LiteralPath -eq "HKLM:\SYSTEM\CurrentControlSet\Services\libvirtualhid_broker" -and
|
||||
$Name -eq "Environment"
|
||||
}
|
||||
}
|
||||
|
||||
It "does nothing when the broker service is absent" {
|
||||
Mock Get-Service { $null }
|
||||
|
||||
Stop-LibVirtualHidBrokerService -Name "libvirtualhid_broker"
|
||||
|
||||
Should -Invoke Stop-Service -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "stops a running broker service" {
|
||||
$service = [pscustomobject]@{
|
||||
Status = "Running"
|
||||
Waited = $false
|
||||
}
|
||||
$service | Add-Member -MemberType ScriptMethod -Name WaitForStatus -Value {
|
||||
param($Status, $Timeout)
|
||||
$null = $Status, $Timeout
|
||||
$this.Waited = $true
|
||||
}
|
||||
Mock Get-Service { $service }
|
||||
|
||||
Stop-LibVirtualHidBrokerService -Name "libvirtualhid_broker" -Confirm:$false
|
||||
|
||||
$service.Waited | Should -BeTrue
|
||||
Should -Invoke Stop-Service -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Name -eq "libvirtualhid_broker" -and $Force
|
||||
}
|
||||
}
|
||||
|
||||
It "skips service registration when no broker executable exists" {
|
||||
Mock Write-Verbose {}
|
||||
|
||||
Install-LibVirtualHidBrokerService
|
||||
|
||||
Should -Invoke Write-Verbose -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Message -like "No libvirtualhid broker executable was found*"
|
||||
}
|
||||
}
|
||||
|
||||
It "registers and configures a new broker service" {
|
||||
$path = Join-Path $TestDrive "libvirtualhid_broker.exe"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
$resolvedPath = (Resolve-Path $path).Path
|
||||
Mock Get-Service { $null }
|
||||
Mock New-Service {}
|
||||
Mock Assert-LibVirtualHidBrokerServiceImagePath {}
|
||||
Mock Clear-LibVirtualHidBrokerServiceEnvironment {}
|
||||
Mock Invoke-CheckedCommand {}
|
||||
Mock Start-Service {}
|
||||
|
||||
Install-LibVirtualHidBrokerService -Path $path -Confirm:$false
|
||||
|
||||
Should -Invoke New-Service -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Name -eq "libvirtualhid_broker" -and
|
||||
$BinaryPathName -eq ('"' + $resolvedPath + '"') -and
|
||||
$StartupType -eq "Automatic"
|
||||
}
|
||||
Should -Invoke Assert-LibVirtualHidBrokerServiceImagePath -Times 1 -Exactly -Scope It
|
||||
Should -Invoke Clear-LibVirtualHidBrokerServiceEnvironment -Times 1 -Exactly -Scope It
|
||||
Should -Invoke Invoke-CheckedCommand -Times 2 -Exactly -Scope It
|
||||
Should -Invoke Start-Service -Times 1 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "updates and configures an existing broker service" {
|
||||
$path = Join-Path $TestDrive "existing-libvirtualhid_broker.exe"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
Mock Get-Service { [pscustomobject]@{ Status = "Stopped" } }
|
||||
Mock Stop-LibVirtualHidBrokerService {}
|
||||
Mock Assert-LibVirtualHidBrokerServiceImagePath {}
|
||||
Mock Clear-LibVirtualHidBrokerServiceEnvironment {}
|
||||
Mock Invoke-CheckedCommand {}
|
||||
Mock Start-Service {}
|
||||
|
||||
Install-LibVirtualHidBrokerService -Path $path -Confirm:$false
|
||||
|
||||
Should -Invoke Stop-LibVirtualHidBrokerService -Times 1 -Exactly -Scope It
|
||||
Should -Invoke Invoke-CheckedCommand -Times 3 -Exactly -Scope It
|
||||
Should -Invoke Invoke-CheckedCommand -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$FilePath -eq "sc.exe" -and $Arguments[0] -eq "config"
|
||||
}
|
||||
Should -Invoke Start-Service -Times 1 -Exactly -Scope It
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Driver installation helpers" {
|
||||
It "imports an existing certificate into both required stores" {
|
||||
$path = Join-Path $TestDrive "driver.cer"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
Mock Import-Certificate {}
|
||||
|
||||
Import-DriverCertificate -Path $path -Confirm:$false
|
||||
|
||||
Should -Invoke Import-Certificate -Times 2 -Exactly -Scope It -ParameterFilter {
|
||||
$FilePath -eq (Resolve-Path $path).Path -and
|
||||
$CertStoreLocation -in @("Cert:\LocalMachine\Root", "Cert:\LocalMachine\TrustedPublisher")
|
||||
}
|
||||
}
|
||||
|
||||
It "ignores a missing certificate" {
|
||||
Mock Import-Certificate {}
|
||||
|
||||
Import-DriverCertificate -Path (Join-Path $TestDrive "missing.cer")
|
||||
|
||||
Should -Invoke Import-Certificate -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "removes a stale device with pnputil" {
|
||||
Mock Invoke-CheckedCommand {}
|
||||
|
||||
Remove-DeviceInstance -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false
|
||||
|
||||
Should -Invoke Invoke-CheckedCommand -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$FilePath -eq "pnputil.exe" -and
|
||||
$Arguments[0] -eq "/remove-device" -and
|
||||
$Arguments[1] -eq "ROOT\LIBVIRTUALHID\0000"
|
||||
}
|
||||
}
|
||||
|
||||
It "sets VhfMode on an existing root device" {
|
||||
Mock Test-Path { $true }
|
||||
Mock New-ItemProperty {}
|
||||
|
||||
Set-RootDeviceVhfMode -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false
|
||||
|
||||
Should -Invoke New-ItemProperty -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Name -eq "VhfMode" -and $Value -eq 1 -and $PropertyType -eq "DWord"
|
||||
}
|
||||
}
|
||||
|
||||
It "skips VhfMode when the registry device is absent" {
|
||||
Mock Test-Path { $false }
|
||||
Mock New-ItemProperty {}
|
||||
Mock Write-Verbose {}
|
||||
|
||||
Set-RootDeviceVhfMode -InstanceId "ROOT\LIBVIRTUALHID\0000"
|
||||
|
||||
Should -Invoke New-ItemProperty -Times 0 -Exactly -Scope It
|
||||
Should -Invoke Write-Verbose -Times 1 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "warns when restarting the device requires a reboot" {
|
||||
Mock pnputil.exe {
|
||||
$global:LASTEXITCODE = 0
|
||||
"A reboot is needed to complete the operation."
|
||||
}
|
||||
Mock Write-Warning {}
|
||||
|
||||
Restart-RootDevice -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false
|
||||
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Message -like "Windows reported that a reboot is required*"
|
||||
}
|
||||
}
|
||||
|
||||
It "throws when restarting the device fails" {
|
||||
Mock pnputil.exe { $global:LASTEXITCODE = 7 }
|
||||
|
||||
{
|
||||
Restart-RootDevice -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false
|
||||
} | Should -Throw "*exited with code 7*"
|
||||
}
|
||||
|
||||
It "updates a driver and permits SetupAPI reboot status" {
|
||||
Mock Invoke-CheckedCommand { $global:LASTEXITCODE = 3010 }
|
||||
Mock Write-Warning {}
|
||||
|
||||
Update-RootDeviceDriverWithSetupApi `
|
||||
-Path "driver.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID" `
|
||||
-SetupHelperPath "setup.exe" `
|
||||
-Confirm:$false
|
||||
|
||||
Should -Invoke Invoke-CheckedCommand -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$FilePath -eq "setup.exe" -and
|
||||
$Arguments[0] -eq "update" -and
|
||||
$SuccessExitCodes -contains 3010
|
||||
}
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "installs a root device through the SetupAPI helper" {
|
||||
Mock Invoke-CheckedCommand {}
|
||||
|
||||
Install-RootDeviceWithSetupApi `
|
||||
-Path "driver.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID" `
|
||||
-SetupHelperPath "setup.exe"
|
||||
|
||||
Should -Invoke Invoke-CheckedCommand -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$FilePath -eq "setup.exe" -and $Arguments -join "," -eq "install,driver.inf,ROOT\LIBVIRTUALHID"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
BeforeAll {
|
||||
$sourcePath = Join-Path $PSScriptRoot "..\..\scripts\windows\libvirtualhid-driver-common.ps1"
|
||||
. $sourcePath
|
||||
}
|
||||
|
||||
Describe "Transcript helpers" {
|
||||
BeforeEach {
|
||||
$script:LibVirtualHidTranscriptStarted = $false
|
||||
Mock New-Item {}
|
||||
Mock Start-Transcript {}
|
||||
Mock Stop-Transcript {}
|
||||
Mock Write-Warning {}
|
||||
}
|
||||
|
||||
It "does nothing when no transcript path is supplied" {
|
||||
Start-LibVirtualHidTranscript
|
||||
|
||||
Should -Invoke Start-Transcript -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "creates the log directory and starts and stops a transcript" {
|
||||
Start-LibVirtualHidTranscript -Path "C:\logs\driver.log"
|
||||
Stop-LibVirtualHidTranscript
|
||||
|
||||
$script:LibVirtualHidTranscriptStarted | Should -BeTrue
|
||||
Should -Invoke New-Item -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$ItemType -eq "Directory" -and $Path -eq "C:\logs"
|
||||
}
|
||||
Should -Invoke Start-Transcript -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Path -eq "C:\logs\driver.log" -and $Append
|
||||
}
|
||||
Should -Invoke Stop-Transcript -Times 1 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "does not stop a transcript that was never started" {
|
||||
Stop-LibVirtualHidTranscript
|
||||
|
||||
Should -Invoke Stop-Transcript -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "warns when starting the transcript fails" {
|
||||
Mock Start-Transcript { throw "start failed" }
|
||||
|
||||
Start-LibVirtualHidTranscript -Path "driver.log"
|
||||
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Message -eq "Unable to start libvirtualhid driver transcript: start failed"
|
||||
}
|
||||
}
|
||||
|
||||
It "warns when stopping the transcript fails" {
|
||||
$script:LibVirtualHidTranscriptStarted = $true
|
||||
Mock Stop-Transcript { throw "stop failed" }
|
||||
|
||||
Stop-LibVirtualHidTranscript
|
||||
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Message -eq "Unable to stop libvirtualhid driver transcript: stop failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get-LibVirtualHidRootDeviceInstanceId" {
|
||||
BeforeEach {
|
||||
Mock Write-Verbose {}
|
||||
}
|
||||
|
||||
It "returns instance identifiers reported by pnputil" {
|
||||
Mock pnputil.exe {
|
||||
$global:LASTEXITCODE = 0
|
||||
@(
|
||||
"Instance ID: ROOT\LIBVIRTUALHID\0000",
|
||||
" Instance ID : ROOT\LIBVIRTUALHID\0001 "
|
||||
)
|
||||
}
|
||||
Mock Get-CimInstance { throw "CIM should not be used" }
|
||||
|
||||
$result = @(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId "ROOT\LIBVIRTUALHID")
|
||||
|
||||
$result | Should -Be @("ROOT\LIBVIRTUALHID\0000", "ROOT\LIBVIRTUALHID\0001")
|
||||
Should -Invoke Get-CimInstance -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "falls back to matching CIM devices" {
|
||||
Mock pnputil.exe { $global:LASTEXITCODE = 1 }
|
||||
Mock Get-CimInstance {
|
||||
@(
|
||||
[pscustomobject]@{
|
||||
PNPDeviceID = "ROOT\LIBVIRTUALHID\0000"
|
||||
HardwareID = @("ROOT\LIBVIRTUALHID")
|
||||
},
|
||||
[pscustomobject]@{
|
||||
PNPDeviceID = "ROOT\OTHER\0000"
|
||||
HardwareID = @("ROOT\OTHER")
|
||||
},
|
||||
[pscustomobject]@{
|
||||
PNPDeviceID = "CUSTOM\INSTANCE"
|
||||
HardwareID = @("ROOT\LIBVIRTUALHID")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$result = @(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId "ROOT\LIBVIRTUALHID")
|
||||
|
||||
$result | Should -Be @("ROOT\LIBVIRTUALHID\0000", "CUSTOM\INSTANCE")
|
||||
}
|
||||
|
||||
It "returns an empty collection when both enumerators fail" {
|
||||
Mock pnputil.exe { throw "pnputil failed" }
|
||||
Mock Get-CimInstance { throw "CIM failed" }
|
||||
|
||||
@(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId "ROOT\LIBVIRTUALHID").Count |
|
||||
Should -Be 0
|
||||
Should -Invoke Write-Verbose -Times 2 -Exactly -Scope It
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get-LibVirtualHidRegistryRootDevice" {
|
||||
BeforeEach {
|
||||
Mock Get-ChildItem {
|
||||
if ($LiteralPath -eq "HKLM:\SYSTEM\CurrentControlSet\Enum\ROOT") {
|
||||
return [pscustomobject]@{
|
||||
PSChildName = "LIBVIRTUALHID"
|
||||
PSPath = "root-key"
|
||||
}
|
||||
}
|
||||
if ($LiteralPath -eq "root-key") {
|
||||
return [pscustomobject]@{
|
||||
PSChildName = "0000"
|
||||
PSPath = "instance-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
Mock Write-Verbose {}
|
||||
}
|
||||
|
||||
It "reports exact hardware IDs and legacy HID class state" {
|
||||
Mock Get-ItemProperty {
|
||||
if ($Name -eq "HardwareID") {
|
||||
return [pscustomobject]@{ HardwareID = @("ROOT\LIBVIRTUALHID") }
|
||||
}
|
||||
return [pscustomobject]@{ ClassGUID = "{745A17A0-74D3-11D0-B6FE-00A0C90F57DA}" }
|
||||
}
|
||||
|
||||
$result = @(Get-LibVirtualHidRegistryRootDevice -TargetHardwareId "ROOT\LIBVIRTUALHID")
|
||||
|
||||
$result.Count | Should -Be 1
|
||||
$result[0].InstanceId | Should -Be "ROOT\LIBVIRTUALHID\0000"
|
||||
$result[0].HasExactHardwareId | Should -BeTrue
|
||||
$result[0].HasCorruptHardwareId | Should -BeFalse
|
||||
$result[0].HasLegacyHidClass | Should -BeTrue
|
||||
}
|
||||
|
||||
It "recognizes a hardware ID split across corrupt registry values" {
|
||||
Mock Get-ItemProperty {
|
||||
if ($Name -eq "HardwareID") {
|
||||
return [pscustomobject]@{ HardwareID = @("ROOT\LIBVIRTUAL", "HID") }
|
||||
}
|
||||
throw "properties unavailable"
|
||||
}
|
||||
|
||||
$result = @(Get-LibVirtualHidRegistryRootDevice -TargetHardwareId "ROOT\LIBVIRTUALHID")
|
||||
|
||||
$result.Count | Should -Be 1
|
||||
$result[0].HasExactHardwareId | Should -BeFalse
|
||||
$result[0].HasCorruptHardwareId | Should -BeTrue
|
||||
$result[0].HasLegacyHidClass | Should -BeFalse
|
||||
Should -Invoke Write-Verbose -Times 1 -Exactly -Scope It
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
BeforeAll {
|
||||
$sourcePath = Join-Path $PSScriptRoot "..\..\scripts\windows\sign-driver-package.ps1"
|
||||
. $sourcePath -PackagePath "."
|
||||
|
||||
function global:Invoke-LibVirtualHidSignTestCommand {
|
||||
param([Parameter(ValueFromRemainingArguments)] $Arguments)
|
||||
|
||||
$null = $Arguments
|
||||
$global:LASTEXITCODE = [int] $env:LIBVIRTUALHID_SIGN_TEST_EXIT_CODE
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Item -LiteralPath Function:\Invoke-LibVirtualHidSignTestCommand -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\LIBVIRTUALHID_SIGN_TEST_EXIT_CODE -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Describe "Find-SignTool" {
|
||||
It "uses signtool from PATH when available" {
|
||||
Mock Get-Command {
|
||||
[pscustomobject]@{ Source = "C:\tools\signtool.exe" }
|
||||
}
|
||||
|
||||
Find-SignTool | Should -Be "C:\tools\signtool.exe"
|
||||
}
|
||||
|
||||
It "finds the newest x64 SDK signtool" {
|
||||
Mock Get-Command { $null }
|
||||
Mock Test-Path { $true }
|
||||
Mock Get-ChildItem {
|
||||
@(
|
||||
[pscustomobject]@{ FullName = "C:\sdk\10.0.1\x64\signtool.exe" },
|
||||
[pscustomobject]@{ FullName = "C:\sdk\10.0.2\x86\signtool.exe" },
|
||||
[pscustomobject]@{ FullName = "C:\sdk\10.0.3\x64\signtool.exe" }
|
||||
)
|
||||
}
|
||||
$previousWindowsSdkDir = $env:WindowsSdkDir
|
||||
try {
|
||||
$env:WindowsSdkDir = "C:\sdk"
|
||||
|
||||
Find-SignTool | Should -Be "C:\sdk\10.0.3\x64\signtool.exe"
|
||||
} finally {
|
||||
$env:WindowsSdkDir = $previousWindowsSdkDir
|
||||
}
|
||||
}
|
||||
|
||||
It "throws when no signtool is installed" {
|
||||
Mock Get-Command { $null }
|
||||
Mock Test-Path { $false }
|
||||
$previousWindowsSdkDir = $env:WindowsSdkDir
|
||||
$previousWdkContentRoot = $env:WDKContentRoot
|
||||
try {
|
||||
$env:WindowsSdkDir = $null
|
||||
$env:WDKContentRoot = $null
|
||||
|
||||
{ Find-SignTool } | Should -Throw "signtool.exe was not found*"
|
||||
} finally {
|
||||
$env:WindowsSdkDir = $previousWindowsSdkDir
|
||||
$env:WDKContentRoot = $previousWdkContentRoot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "sign-driver-package.ps1 entry point" {
|
||||
It "executes normal invocation and requires the driver catalog" {
|
||||
{
|
||||
& $sourcePath -PackagePath $TestDrive
|
||||
} | Should -Throw "Driver catalog was not found:*"
|
||||
}
|
||||
|
||||
It "creates, exports, uses, and removes a temporary signing certificate" {
|
||||
$packagePath = Join-Path $TestDrive "driver"
|
||||
$certificatePath = Join-Path $TestDrive "certificates\driver.cer"
|
||||
New-Item -ItemType Directory -Path $packagePath | Out-Null
|
||||
New-Item -ItemType File -Path (Join-Path $packagePath "libvirtualhid.cat") | Out-Null
|
||||
Mock New-SelfSignedCertificate {
|
||||
[pscustomobject]@{ Thumbprint = "ABC123" }
|
||||
}
|
||||
Mock Export-Certificate {} -RemoveParameterType Cert
|
||||
Mock Get-Command {
|
||||
[pscustomobject]@{ Source = "Invoke-LibVirtualHidSignTestCommand" }
|
||||
}
|
||||
Mock Remove-Item {}
|
||||
$env:LIBVIRTUALHID_SIGN_TEST_EXIT_CODE = 0
|
||||
|
||||
& $sourcePath `
|
||||
-PackagePath $packagePath `
|
||||
-CertificatePath $certificatePath `
|
||||
-ValidDays 3
|
||||
|
||||
Should -Invoke New-SelfSignedCertificate -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Subject -eq "CN=libvirtualhid CI Test Driver Signing" -and
|
||||
$Type -eq "CodeSigningCert" -and
|
||||
$KeyLength -eq 3072
|
||||
}
|
||||
Should -Invoke Export-Certificate -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Cert.Thumbprint -eq "ABC123" -and $FilePath -eq $certificatePath
|
||||
}
|
||||
Should -Invoke Remove-Item -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$LiteralPath -eq "Cert:\CurrentUser\My\ABC123"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-CheckedCommand" {
|
||||
It "returns after a successful signing command" {
|
||||
$env:LIBVIRTUALHID_SIGN_TEST_EXIT_CODE = 0
|
||||
|
||||
{
|
||||
Invoke-CheckedCommand `
|
||||
-FilePath "Invoke-LibVirtualHidSignTestCommand" `
|
||||
-Arguments @("sign", "driver.cat")
|
||||
} | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "throws after a failed signing command" {
|
||||
$env:LIBVIRTUALHID_SIGN_TEST_EXIT_CODE = 9
|
||||
|
||||
{
|
||||
Invoke-CheckedCommand `
|
||||
-FilePath "Invoke-LibVirtualHidSignTestCommand" `
|
||||
-Arguments @("sign", "driver.cat")
|
||||
} | Should -Throw "*exited with code 9*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
BeforeAll {
|
||||
$sourcePath = Join-Path $PSScriptRoot "..\..\scripts\windows\test-browser-gamepad.ps1"
|
||||
. $sourcePath -GamepadAdapterPath "unused.exe"
|
||||
}
|
||||
|
||||
Describe "Get-ExpectedGamepadIdPattern" {
|
||||
It "returns an identifying expression for <profile>" -ForEach @(
|
||||
@{ profile = "generic"; expected = "1209" }
|
||||
@{ profile = "x360"; expected = "028e" }
|
||||
@{ profile = "xone"; expected = "02ea" }
|
||||
@{ profile = "xseries"; expected = "0b12" }
|
||||
@{ profile = "ds4"; expected = "05c4" }
|
||||
@{ profile = "ds5"; expected = "0ce6" }
|
||||
@{ profile = "switch"; expected = "2009" }
|
||||
) {
|
||||
Get-ExpectedGamepadIdPattern -ProfileName $profile | Should -Match $expected
|
||||
}
|
||||
|
||||
It "rejects an unsupported profile" {
|
||||
{ Get-ExpectedGamepadIdPattern -ProfileName "unknown" } |
|
||||
Should -Throw "Unsupported profile: unknown"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "test-browser-gamepad.ps1 entry point" {
|
||||
It "executes normal invocation and validates the polling lifetime" {
|
||||
{
|
||||
& $sourcePath `
|
||||
-GamepadAdapterPath "unused.exe" `
|
||||
-TimeoutSeconds 20 `
|
||||
-HoldSeconds 20
|
||||
} | Should -Throw "-HoldSeconds must be greater than -TimeoutSeconds*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Resolve-BrowserPath" {
|
||||
It "resolves an explicitly supplied browser path" {
|
||||
$path = Join-Path $TestDrive "browser.exe"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
|
||||
Resolve-BrowserPath -Path $path | Should -Be (Resolve-Path $path).Path
|
||||
}
|
||||
|
||||
It "selects the first installed supported browser" {
|
||||
Mock Test-Path { $LiteralPath -like "*Microsoft\Edge\Application\msedge.exe" }
|
||||
|
||||
Resolve-BrowserPath | Should -Match "Microsoft\\Edge\\Application\\msedge.exe$"
|
||||
}
|
||||
|
||||
It "requires an installed supported browser" {
|
||||
Mock Test-Path { $false }
|
||||
|
||||
{ Resolve-BrowserPath } | Should -Throw "No supported browser was found*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get-FreeTcpPort" {
|
||||
It "returns an available ephemeral loopback port" {
|
||||
Get-FreeTcpPort | Should -BeGreaterThan 0
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Wait-ForDevToolsJson" {
|
||||
It "returns JSON from the browser endpoint" {
|
||||
$response = [pscustomobject]@{ Browser = "Edge" }
|
||||
Mock Invoke-RestMethod { $response }
|
||||
|
||||
Wait-ForDevToolsJson -Port 9222 -Path "/json/version" -TimeoutSeconds 1 |
|
||||
Should -Be $response
|
||||
}
|
||||
|
||||
It "times out after repeated endpoint failures" {
|
||||
Mock Invoke-RestMethod { throw "not ready" }
|
||||
Mock Start-Sleep {}
|
||||
|
||||
{
|
||||
Wait-ForDevToolsJson -Port 9222 -Path "/json/version" -TimeoutSeconds 0
|
||||
} | Should -Throw "Timed out waiting for browser DevTools endpoint*"
|
||||
Should -Invoke Start-Sleep -Times 1 -Exactly -Scope It
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Wait-ForDevToolsPageTarget" {
|
||||
It "selects the page matching the expected URL" {
|
||||
Mock Wait-ForDevToolsJson {
|
||||
@(
|
||||
[pscustomobject]@{
|
||||
type = "page"
|
||||
url = "edge://newtab"
|
||||
webSocketDebuggerUrl = "ws://127.0.0.1/devtools/page/newtab"
|
||||
},
|
||||
[pscustomobject]@{
|
||||
type = "page"
|
||||
url = "https://hardwaretester.com/gamepad?test=1"
|
||||
webSocketDebuggerUrl = "ws://127.0.0.1/devtools/page/test"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$result = Wait-ForDevToolsPageTarget `
|
||||
-Port 9222 `
|
||||
-ExpectedUrl "https://hardwaretester.com/gamepad" `
|
||||
-TimeoutSeconds 1
|
||||
|
||||
$result.url | Should -Be "https://hardwaretester.com/gamepad?test=1"
|
||||
$result.webSocketDebuggerUrl | Should -Be "ws://127.0.0.1/devtools/page/test"
|
||||
}
|
||||
|
||||
It "falls back to the first non-browser-internal page" {
|
||||
Mock Wait-ForDevToolsJson {
|
||||
@(
|
||||
[pscustomobject]@{
|
||||
type = "page"
|
||||
url = "chrome://newtab"
|
||||
webSocketDebuggerUrl = "ws://127.0.0.1/devtools/page/newtab"
|
||||
},
|
||||
[pscustomobject]@{
|
||||
type = "page"
|
||||
url = "https://example.com/"
|
||||
webSocketDebuggerUrl = "ws://127.0.0.1/devtools/page/fallback"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$result = Wait-ForDevToolsPageTarget `
|
||||
-Port 9222 `
|
||||
-ExpectedUrl "https://hardwaretester.com/gamepad" `
|
||||
-TimeoutSeconds 1
|
||||
|
||||
$result.url | Should -Be "https://example.com/"
|
||||
}
|
||||
|
||||
It "times out when no page target becomes available" {
|
||||
Mock Wait-ForDevToolsJson { @() }
|
||||
Mock Start-Sleep {}
|
||||
|
||||
{
|
||||
Wait-ForDevToolsPageTarget `
|
||||
-Port 9222 `
|
||||
-ExpectedUrl "https://hardwaretester.com/gamepad" `
|
||||
-TimeoutSeconds 0
|
||||
} | Should -Throw "Timed out waiting for a browser page target."
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get-GamepadApiProbeExpression" {
|
||||
It "embeds the expected pattern, timeout, and strict matching mode" {
|
||||
$expression = Get-GamepadApiProbeExpression `
|
||||
-ExpectedIdPattern 'xbox "series"' `
|
||||
-AllowAnyGamepad $false `
|
||||
-TimeoutSeconds 17
|
||||
|
||||
$expression | Should -Match 'new RegExp\("xbox \\"series\\"", "i"\)'
|
||||
$expression | Should -Match "const allowAnyGamepad = false;"
|
||||
$expression | Should -Match "Date.now\(\) \+ \(17 \* 1000\)"
|
||||
}
|
||||
|
||||
It "enables any-gamepad matching when requested" {
|
||||
Get-GamepadApiProbeExpression `
|
||||
-ExpectedIdPattern "ignored" `
|
||||
-AllowAnyGamepad $true `
|
||||
-TimeoutSeconds 1 |
|
||||
Should -Match "const allowAnyGamepad = true;"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
BeforeAll {
|
||||
$sourcePath = Join-Path $PSScriptRoot "..\..\scripts\windows\test-installed-driver.ps1"
|
||||
. $sourcePath
|
||||
}
|
||||
|
||||
Describe "Invoke-PnPUtil" {
|
||||
It "returns pnputil output on success" {
|
||||
Mock pnputil.exe {
|
||||
$global:LASTEXITCODE = 0
|
||||
@("first", "second")
|
||||
}
|
||||
|
||||
@(Invoke-PnPUtil -Arguments @("/enum-devices")) | Should -Be @("first", "second")
|
||||
}
|
||||
|
||||
It "throws with command output on failure" {
|
||||
Mock pnputil.exe {
|
||||
$global:LASTEXITCODE = 6
|
||||
"access denied"
|
||||
}
|
||||
|
||||
{
|
||||
Invoke-PnPUtil -Arguments @("/enum-devices")
|
||||
} | Should -Throw "*exited with code 6*access denied*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "test-installed-driver.ps1 entry point" {
|
||||
It "executes normal invocation and requires an installed root device" {
|
||||
function pnputil.exe {
|
||||
$global:LASTEXITCODE = 0
|
||||
@()
|
||||
}
|
||||
|
||||
try {
|
||||
{
|
||||
& $sourcePath -HardwareId "ROOT\LIBVIRTUALHID_PESTER"
|
||||
} | Should -Throw "No installed libvirtualhid root device was found*"
|
||||
} finally {
|
||||
Remove-Item -LiteralPath Function:\pnputil.exe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "PnPUtil device output parsing" {
|
||||
It "creates an empty record with a trimmed instance ID" {
|
||||
$record = ConvertTo-PnPUtilDeviceRecord -InstanceId " ROOT\LIBVIRTUALHID\0000 "
|
||||
|
||||
$record.InstanceId | Should -Be "ROOT\LIBVIRTUALHID\0000"
|
||||
$record.HardwareIds.Count | Should -Be 0
|
||||
$record.Status | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It "parses single-value fields and hardware ID continuations" {
|
||||
$record = ConvertTo-PnPUtilDeviceRecord -InstanceId "ROOT\LIBVIRTUALHID\0000"
|
||||
|
||||
$section = ConvertFrom-PnPUtilDeviceLine `
|
||||
-Record $record `
|
||||
-Line "Device Description: libvirtualhid" `
|
||||
-Section $null
|
||||
$section | Should -BeNullOrEmpty
|
||||
$section = ConvertFrom-PnPUtilDeviceLine `
|
||||
-Record $record `
|
||||
-Line "Hardware IDs: ROOT\LIBVIRTUALHID" `
|
||||
-Section $section
|
||||
$section | Should -Be "HardwareIds"
|
||||
$section = ConvertFrom-PnPUtilDeviceLine `
|
||||
-Record $record `
|
||||
-Line " ROOT\LIBVIRTUALHID_COMPAT" `
|
||||
-Section $section
|
||||
|
||||
$record.DeviceDescription | Should -Be "libvirtualhid"
|
||||
$record.HardwareIds | Should -Be @("ROOT\LIBVIRTUALHID", "ROOT\LIBVIRTUALHID_COMPAT")
|
||||
}
|
||||
|
||||
It "parses multiple complete device records" {
|
||||
$output = @(
|
||||
"Microsoft PnP Utility",
|
||||
"Instance ID: ROOT\LIBVIRTUALHID\0000",
|
||||
"Device Description: libvirtualhid root",
|
||||
"Driver Name: libvirtualhid.inf",
|
||||
"Status: Started",
|
||||
"Problem Code: 0",
|
||||
"Problem Status: 0x0",
|
||||
"Hardware IDs: ROOT\LIBVIRTUALHID",
|
||||
"Instance ID: HID\VID_1209&PID_0001\0000",
|
||||
"Device Description: virtual gamepad",
|
||||
"Status: Stopped"
|
||||
)
|
||||
|
||||
$records = @(ConvertFrom-PnPUtilDeviceOutput -Output $output)
|
||||
|
||||
$records.Count | Should -Be 2
|
||||
$records[0].Status | Should -Be "Started"
|
||||
$records[0].DriverName | Should -Be "libvirtualhid.inf"
|
||||
$records[0].ProblemCode | Should -Be "0"
|
||||
$records[0].ProblemStatus | Should -Be "0x0"
|
||||
$records[1].InstanceId | Should -Be "HID\VID_1209&PID_0001\0000"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "PnP device validation" {
|
||||
It "writes every available device field as verbose output" {
|
||||
$record = [pscustomobject]@{
|
||||
InstanceId = "ROOT\LIBVIRTUALHID\0000"
|
||||
DeviceDescription = "libvirtualhid root"
|
||||
Status = "Started"
|
||||
DriverName = "libvirtualhid.inf"
|
||||
HardwareIds = @("ROOT\LIBVIRTUALHID")
|
||||
ProblemCode = "0"
|
||||
ProblemStatus = "0x0"
|
||||
}
|
||||
Mock Write-Verbose {}
|
||||
|
||||
Write-PnPRecordVerbose -Record $record
|
||||
|
||||
Should -Invoke Write-Verbose -Times 7 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "filters device records by instance ID and hardware ID" {
|
||||
Mock Invoke-PnPUtil {
|
||||
@(
|
||||
"Instance ID: ROOT\LIBVIRTUALHID\0000",
|
||||
"Status: Started",
|
||||
"Instance ID: CUSTOM\0000",
|
||||
"Hardware IDs: ROOT\LIBVIRTUALHID",
|
||||
"Status: Started",
|
||||
"Instance ID: ROOT\OTHER\0000",
|
||||
"Status: Started"
|
||||
)
|
||||
}
|
||||
Mock Write-PnPRecordVerbose {}
|
||||
|
||||
$result = @(Get-PnPUtilDevicesByDeviceId -DeviceId "ROOT\LIBVIRTUALHID")
|
||||
|
||||
$result.Count | Should -Be 2
|
||||
Should -Invoke Write-PnPRecordVerbose -Times 2 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "accepts a started device" {
|
||||
$record = [pscustomobject]@{
|
||||
InstanceId = "ROOT\LIBVIRTUALHID\0000"
|
||||
Status = "Started"
|
||||
ProblemCode = $null
|
||||
ProblemStatus = $null
|
||||
}
|
||||
|
||||
{ Assert-StartedPnPRecord -Record $record -Description "Root device" } |
|
||||
Should -Not -Throw
|
||||
}
|
||||
|
||||
It "reports status and problem details for a stopped device" {
|
||||
$record = [pscustomobject]@{
|
||||
InstanceId = "ROOT\LIBVIRTUALHID\0000"
|
||||
Status = "Stopped"
|
||||
ProblemCode = "28"
|
||||
ProblemStatus = "0xC0000490"
|
||||
}
|
||||
|
||||
{ Assert-StartedPnPRecord -Record $record -Description "Root device" } |
|
||||
Should -Throw "*Problem Code: 28*Problem Status: 0xC0000490*"
|
||||
}
|
||||
|
||||
It "requires at least one root device" {
|
||||
Mock Get-PnPUtilDevicesByDeviceId { @() }
|
||||
|
||||
{ Assert-RootDeviceStarted -TargetHardwareId "ROOT\LIBVIRTUALHID" } |
|
||||
Should -Throw "No installed libvirtualhid root device was found*"
|
||||
}
|
||||
|
||||
It "validates every matching root device" {
|
||||
Mock Get-PnPUtilDevicesByDeviceId {
|
||||
@(
|
||||
[pscustomobject]@{ InstanceId = "one"; Status = "Started" },
|
||||
[pscustomobject]@{ InstanceId = "two"; Status = "Started" }
|
||||
)
|
||||
}
|
||||
Mock Assert-StartedPnPRecord {}
|
||||
|
||||
Assert-RootDeviceStarted -TargetHardwareId "ROOT\LIBVIRTUALHID"
|
||||
|
||||
Should -Invoke Assert-StartedPnPRecord -Times 2 -Exactly -Scope It
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Control device validation" {
|
||||
It "opens an existing file for shared read and write access" {
|
||||
$path = Join-Path $TestDrive "control-device"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
|
||||
{ Assert-ControlDeviceOpen -Path $path } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "explains when the control device cannot be opened" {
|
||||
{
|
||||
Assert-ControlDeviceOpen -Path (Join-Path $TestDrive "missing-device")
|
||||
} | Should -Throw "Could not open*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Gamepad profile metadata" {
|
||||
It "returns the expected hardware ID for <profile>" -ForEach @(
|
||||
@{ profile = "generic"; expected = "HID\VID_1209&PID_0001" }
|
||||
@{ profile = "xone"; expected = "HID\VID_045E&PID_02EA&IG_00" }
|
||||
@{ profile = "xseries"; expected = "HID\VID_045E&PID_0B12&IG_00" }
|
||||
@{ profile = "ds4"; expected = "HID\VID_054C&PID_05C4" }
|
||||
@{ profile = "ds5"; expected = "HID\VID_054C&PID_0CE6" }
|
||||
@{ profile = "switch"; expected = "HID\VID_057E&PID_2009" }
|
||||
) {
|
||||
@(Get-ExpectedGamepadHardwareId -ProfileName $profile) | Should -Be @($expected)
|
||||
}
|
||||
|
||||
It "rejects an unsupported profile" {
|
||||
{ Get-ExpectedGamepadHardwareId -ProfileName "unknown" } |
|
||||
Should -Throw "Unsupported profile: unknown"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Gamepad device waits" {
|
||||
BeforeEach {
|
||||
Mock Start-Sleep {}
|
||||
}
|
||||
|
||||
It "accepts a started non-VHF child device" {
|
||||
Mock Get-PnPUtilDevicesByDeviceId {
|
||||
[pscustomobject]@{
|
||||
InstanceId = "HID\GAMEPAD\0000"
|
||||
Status = "Started"
|
||||
DriverName = "xboxgip.inf"
|
||||
DeviceDescription = "Xbox Controller"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Wait-ForStartedGamepadChild -ProfileName "xseries" -TimeoutSeconds 1
|
||||
} | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "rejects a missing gamepad child after the timeout" {
|
||||
Mock Get-PnPUtilDevicesByDeviceId { @() }
|
||||
|
||||
{
|
||||
Wait-ForStartedGamepadChild -ProfileName "xseries" -TimeoutSeconds 0
|
||||
} | Should -Throw "No gamepad child device was found*"
|
||||
}
|
||||
|
||||
It "accepts a started Xbox 360 companion" {
|
||||
Mock Get-PnPUtilDevicesByDeviceId {
|
||||
[pscustomobject]@{
|
||||
InstanceId = "ROOT\LIBVIRTUALHID_XBOX360\0000"
|
||||
Status = "Started"
|
||||
DriverName = "xusb22.inf"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Wait-ForStartedXbox360Companion -TimeoutSeconds 1
|
||||
} | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "rejects a missing Xbox 360 companion after the timeout" {
|
||||
Mock Get-PnPUtilDevicesByDeviceId { @() }
|
||||
|
||||
{
|
||||
Wait-ForStartedXbox360Companion -TimeoutSeconds 0
|
||||
} | Should -Throw "No Xbox 360 XUSB companion was found*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-GamepadAdapterSmoke" {
|
||||
It "does nothing when no adapter path is supplied" {
|
||||
Mock Start-Process { throw "adapter should not start" }
|
||||
|
||||
Invoke-GamepadAdapterSmoke `
|
||||
-ProfileName "xseries" `
|
||||
-HoldSeconds 12 `
|
||||
-DeviceStartTimeoutSeconds 1
|
||||
|
||||
Should -Invoke Start-Process -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "starts, validates, and stops an adapter" {
|
||||
$path = Join-Path $TestDrive "gamepad_adapter.exe"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
$process = [pscustomobject]@{ HasExited = $false; Id = 42 }
|
||||
Mock Start-Process { $process }
|
||||
Mock Start-Sleep {}
|
||||
Mock Wait-ForStartedGamepadChild {}
|
||||
Mock Stop-Process {}
|
||||
Mock Wait-Process {}
|
||||
|
||||
Invoke-GamepadAdapterSmoke `
|
||||
-Path $path `
|
||||
-ProfileName "xseries" `
|
||||
-HoldSeconds 12 `
|
||||
-DeviceStartTimeoutSeconds 1
|
||||
|
||||
Should -Invoke Wait-ForStartedGamepadChild -Times 1 -Exactly -Scope It
|
||||
Should -Invoke Stop-Process -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Id -eq 42 -and $Force
|
||||
}
|
||||
}
|
||||
|
||||
It "uses the Xbox 360 companion validation path" {
|
||||
$path = Join-Path $TestDrive "x360-gamepad_adapter.exe"
|
||||
New-Item -ItemType File -Path $path | Out-Null
|
||||
$process = [pscustomobject]@{ HasExited = $false; Id = 43 }
|
||||
Mock Start-Process { $process }
|
||||
Mock Start-Sleep {}
|
||||
Mock Wait-ForStartedXbox360Companion {}
|
||||
Mock Stop-Process {}
|
||||
Mock Wait-Process {}
|
||||
|
||||
Invoke-GamepadAdapterSmoke `
|
||||
-Path $path `
|
||||
-ProfileName "x360" `
|
||||
-HoldSeconds 12 `
|
||||
-DeviceStartTimeoutSeconds 1
|
||||
|
||||
Should -Invoke Wait-ForStartedXbox360Companion -Times 1 -Exactly -Scope It
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
BeforeAll {
|
||||
$sourcePath = Join-Path $PSScriptRoot "..\..\scripts\windows\uninstall-driver.ps1"
|
||||
. $sourcePath
|
||||
|
||||
function global:Invoke-LibVirtualHidUninstallTestCommand {
|
||||
param([Parameter(ValueFromRemainingArguments)] $Arguments)
|
||||
|
||||
$null = $Arguments
|
||||
$global:LASTEXITCODE = [int] $env:LIBVIRTUALHID_UNINSTALL_TEST_EXIT_CODE
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Item -LiteralPath Function:\Invoke-LibVirtualHidUninstallTestCommand -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\LIBVIRTUALHID_UNINSTALL_TEST_EXIT_CODE -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Describe "Invoke-CheckedCommand" {
|
||||
It "returns after an allowed exit code" {
|
||||
$env:LIBVIRTUALHID_UNINSTALL_TEST_EXIT_CODE = 0
|
||||
|
||||
{
|
||||
Invoke-CheckedCommand `
|
||||
-FilePath "Invoke-LibVirtualHidUninstallTestCommand" `
|
||||
-Arguments @("delete")
|
||||
} | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "throws after a failed command" {
|
||||
$env:LIBVIRTUALHID_UNINSTALL_TEST_EXIT_CODE = 3
|
||||
|
||||
{
|
||||
Invoke-CheckedCommand `
|
||||
-FilePath "Invoke-LibVirtualHidUninstallTestCommand" `
|
||||
-Arguments @("delete")
|
||||
} | Should -Throw "*exited with code 3*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "uninstall-driver.ps1 entry point" {
|
||||
It "executes normal invocation and validates an explicit package name" {
|
||||
{
|
||||
& $sourcePath -PublishedName "lib.inf"
|
||||
} | Should -Throw "The published driver package name is invalid*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Remove-LibVirtualHidBrokerService" {
|
||||
It "does nothing when the service is absent" {
|
||||
Mock Get-Service { $null }
|
||||
Mock Stop-Service {}
|
||||
Mock Invoke-CheckedCommand {}
|
||||
|
||||
Remove-LibVirtualHidBrokerService -Name "libvirtualhid_broker"
|
||||
|
||||
Should -Invoke Stop-Service -Times 0 -Exactly -Scope It
|
||||
Should -Invoke Invoke-CheckedCommand -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "stops and deletes an installed service" {
|
||||
$service = [pscustomobject]@{
|
||||
Status = "Running"
|
||||
Disposed = $false
|
||||
Waited = $false
|
||||
}
|
||||
$service | Add-Member -MemberType ScriptMethod -Name WaitForStatus -Value {
|
||||
param($Status, $Timeout)
|
||||
$null = $Status, $Timeout
|
||||
$this.Waited = $true
|
||||
}
|
||||
$service | Add-Member -MemberType ScriptMethod -Name Dispose -Value {
|
||||
$this.Disposed = $true
|
||||
}
|
||||
$script:getServiceCalls = 0
|
||||
Mock Get-Service {
|
||||
$script:getServiceCalls += 1
|
||||
if ($script:getServiceCalls -eq 1) {
|
||||
return $service
|
||||
}
|
||||
return $null
|
||||
}
|
||||
Mock Stop-Service {}
|
||||
Mock Invoke-CheckedCommand {}
|
||||
|
||||
Remove-LibVirtualHidBrokerService -Name "libvirtualhid_broker" -Confirm:$false
|
||||
|
||||
$service.Waited | Should -BeTrue
|
||||
$service.Disposed | Should -BeTrue
|
||||
Should -Invoke Stop-Service -Times 1 -Exactly -Scope It
|
||||
Should -Invoke Invoke-CheckedCommand -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$FilePath -eq "sc.exe" -and $Arguments -join "," -eq "delete,libvirtualhid_broker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Find-PublishedName" {
|
||||
It "returns matching DISM driver package names" {
|
||||
Mock Get-WindowsDriver {
|
||||
@(
|
||||
[pscustomobject]@{
|
||||
Driver = "oem42.inf"
|
||||
OriginalFileName = "C:\drivers\libvirtualhid.inf"
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Driver = "input.inf"
|
||||
OriginalFileName = "C:\drivers\libvirtualhid.inf"
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Driver = "oem7.inf"
|
||||
OriginalFileName = "C:\drivers\other.inf"
|
||||
}
|
||||
)
|
||||
}
|
||||
Mock Get-CimInstance { throw "CIM should not be used" }
|
||||
|
||||
@(Find-PublishedName `
|
||||
-TargetOriginalName "libvirtualhid.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID") | Should -Be @("oem42.inf")
|
||||
Should -Invoke Get-CimInstance -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "returns an empty collection when DISM succeeds without a match" {
|
||||
Mock Get-WindowsDriver { @() }
|
||||
|
||||
@(Find-PublishedName `
|
||||
-TargetOriginalName "libvirtualhid.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID").Count | Should -Be 0
|
||||
}
|
||||
|
||||
It "falls back to the package bound to the target device" {
|
||||
Mock Get-WindowsDriver { throw "DISM failed" }
|
||||
Mock Get-LibVirtualHidRootDeviceInstanceId { @("ROOT\LIBVIRTUALHID\0000") }
|
||||
Mock Get-CimInstance {
|
||||
@(
|
||||
[pscustomobject]@{
|
||||
DeviceID = "ROOT\LIBVIRTUALHID\0000"
|
||||
InfName = "oem12.inf"
|
||||
},
|
||||
[pscustomobject]@{
|
||||
DeviceID = "ROOT\OTHER\0000"
|
||||
InfName = "oem13.inf"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@(Find-PublishedName `
|
||||
-TargetOriginalName "libvirtualhid.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID") | Should -Be @("oem12.inf")
|
||||
}
|
||||
|
||||
It "explains when DISM and the bound-device fallback are unavailable" {
|
||||
Mock Get-WindowsDriver { throw "DISM failed" }
|
||||
Mock Get-LibVirtualHidRootDeviceInstanceId { @() }
|
||||
|
||||
{
|
||||
Find-PublishedName `
|
||||
-TargetOriginalName "libvirtualhid.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID"
|
||||
} | Should -Throw "*no bound device is available for CIM fallback*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Published package validation" {
|
||||
It "accepts a normal OEM package name" {
|
||||
{ Assert-PublishedName -Name "oem123.inf" } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "rejects an unsafe package name" {
|
||||
{ Assert-PublishedName -Name "libvirtualhid.inf" } | Should -Throw "*is invalid*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Remove-LibVirtualHidDeviceInstance" {
|
||||
It "reports pnputil output without warning on success" {
|
||||
Mock pnputil.exe {
|
||||
$global:LASTEXITCODE = 0
|
||||
"Device removed"
|
||||
}
|
||||
Mock Write-Warning {}
|
||||
|
||||
Remove-LibVirtualHidDeviceInstance `
|
||||
-InstanceId "ROOT\LIBVIRTUALHID\0000" `
|
||||
-Confirm:$false
|
||||
|
||||
Should -Invoke Write-Warning -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "warns and continues after a failed device removal" {
|
||||
Mock pnputil.exe { $global:LASTEXITCODE = 5 }
|
||||
Mock Write-Warning {}
|
||||
|
||||
Remove-LibVirtualHidDeviceInstance `
|
||||
-InstanceId "ROOT\LIBVIRTUALHID\0000" `
|
||||
-Confirm:$false
|
||||
|
||||
Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter {
|
||||
$Message -like "pnputil.exe /remove-device*exited with code 5*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Assert-LibVirtualHidRemoved" {
|
||||
BeforeEach {
|
||||
Mock Get-Service { $null }
|
||||
Mock Get-LibVirtualHidRootDeviceInstanceId { @() }
|
||||
Mock Find-PublishedName { @() }
|
||||
}
|
||||
|
||||
It "accepts a fully removed driver" {
|
||||
{
|
||||
Assert-LibVirtualHidRemoved `
|
||||
-TargetOriginalName "libvirtualhid.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID" `
|
||||
-ServiceName "libvirtualhid_broker"
|
||||
} | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "rejects a remaining service" {
|
||||
Mock Get-Service { [pscustomobject]@{ Name = "libvirtualhid_broker" } }
|
||||
|
||||
{
|
||||
Assert-LibVirtualHidRemoved `
|
||||
-TargetOriginalName "libvirtualhid.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID" `
|
||||
-ServiceName "libvirtualhid_broker"
|
||||
} | Should -Throw "*service remains installed*"
|
||||
}
|
||||
|
||||
It "rejects remaining device instances" {
|
||||
Mock Get-LibVirtualHidRootDeviceInstanceId { @("ROOT\LIBVIRTUALHID\0000") }
|
||||
|
||||
{
|
||||
Assert-LibVirtualHidRemoved `
|
||||
-TargetOriginalName "libvirtualhid.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID" `
|
||||
-ServiceName "libvirtualhid_broker"
|
||||
} | Should -Throw "*device instances remain installed*"
|
||||
}
|
||||
|
||||
It "rejects remaining staged packages" {
|
||||
Mock Find-PublishedName { @("oem4.inf") }
|
||||
|
||||
{
|
||||
Assert-LibVirtualHidRemoved `
|
||||
-TargetOriginalName "libvirtualhid.inf" `
|
||||
-TargetHardwareId "ROOT\LIBVIRTUALHID" `
|
||||
-ServiceName "libvirtualhid_broker"
|
||||
} | Should -Throw "*driver packages remain staged*"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Remove-DriverCertificate" {
|
||||
It "does nothing without a certificate subject" {
|
||||
Mock Get-ChildItem { throw "certificate stores should not be read" }
|
||||
|
||||
Remove-DriverCertificate
|
||||
|
||||
Should -Invoke Get-ChildItem -Times 0 -Exactly -Scope It
|
||||
}
|
||||
|
||||
It "removes only matching self-signed certificates" {
|
||||
Mock Get-ChildItem {
|
||||
@(
|
||||
[pscustomobject]@{
|
||||
Subject = "CN=libvirtualhid Test"
|
||||
Issuer = "CN=libvirtualhid Test"
|
||||
Thumbprint = "AAA"
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Subject = "CN=libvirtualhid Test"
|
||||
Issuer = "CN=Other"
|
||||
Thumbprint = "BBB"
|
||||
}
|
||||
)
|
||||
}
|
||||
Mock Remove-Item {}
|
||||
|
||||
Remove-DriverCertificate -Subject "CN=libvirtualhid Test" -Confirm:$false
|
||||
|
||||
Should -Invoke Remove-Item -Times 2 -Exactly -Scope It -ParameterFilter {
|
||||
$LiteralPath -match "AAA$" -and $Force
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user