PoshPulseAudio.Test.ps1

BeforeDiscovery {
    $Global:testdata = @{}
    foreach($child in (Get-ChildItem $PSScriptRoot/testdata -Filter "*.txt")) {
        $Global:testdata[$child.BaseName] = Get-Content $child
    }
}

BeforeAll {
    Remove-Module PoshPulseAudio -ErrorAction SilentlyContinue
    Import-Module ./PoshPulseAudio.psm1

    . $PSScriptRoot/PADataStructs.ps1

    InModuleScope PoshPulseAudio {
        Mock pactl {
            throw "pactl interation not mocked: pactl $($args -join " ")"
        }
    }
}

Describe 'Get-PACard' {
    BeforeAll {
        InModuleScope PoshPulseAudio {
            Mock pactl {
                $Global:testdata["cards"]
            } -ParameterFilter { ($args -join " ") -eq "list cards" }
        }
    }

    It 'Gets all pulse audio cards' {
        $cards = Get-PACard
        
        $cards.Count | Should -Be 3
        $cards[0].Index | Should -Be 0
        $cards[0].Name | Should -Be "alsa_card.usb-FiiO_DigiHug_USB_Audio-01"
        $cards[0].Driver | Should -Be "module-alsa-card.c"
        $cards[1].Index | Should -Be 1
        $cards[1].Name | Should -Be "alsa_card.usb-C-Media_Electronics_Inc._USB_Audio_Device-00"
        $cards[1].Driver | Should -Be "module-alsa-card.c"
        $cards[2].Index | Should -Be 2
        $cards[2].Name | Should -Be "alsa_card.pci-0000_2d_00.1"
        $cards[2].Driver | Should -Be "module-alsa-card.c"
    }

    It 'Gets a pulse audio card by name' {
        $card = Get-PACard -Name "alsa_card.pci-0000_2d_00.1"
        
        $card.Index | Should -Be 2
        $card.Name | Should -Be "alsa_card.pci-0000_2d_00.1"
    }

    It 'Returns empty when named card is not found' {
        $card = Get-PACard -Name "alsa_card.usb-DNE"

        $card | Should -BeNullOrEmpty
    }

    It 'Gets a pulse audio card by wildcard name match' {
        $cards = Get-PACard -Name "alsa_card.usb-FiiO_DigiHug_USB_Audio-*"
        
        $cards.Count | Should -Be 1
        $cards[0].Name | Should -Be "alsa_card.usb-FiiO_DigiHug_USB_Audio-01"
        $cards[0].Index | Should -Be 0
    }

    It 'Returns multiple cards when multiple matches are found' {
        $cards = Get-PACard -Name "alsa_card.usb-*"
        
        $cards.Count | Should -Be 2
        $cards[0].Name | Should -Be "alsa_card.usb-FiiO_DigiHug_USB_Audio-01"
        $cards[0].Index | Should -Be 0
        $cards[1].Name | Should -Be "alsa_card.usb-C-Media_Electronics_Inc._USB_Audio_Device-00"
        $cards[1].Index | Should -Be 1
    }

    It 'Returns empty when no matching cards are found' {
        $cards = Get-PACard -Name "alsa_card.usb-DNE*"

        $cards.Count | Should -Be 0
    }

    It 'Populates profiles correctly' {
        $card = Get-PACard -Name "alsa_card.usb-FiiO_DigiHug_USB_Audio-01"

        $card.Profiles.Count | Should -Be 9

        $card.Profiles[0].SymbolicName | Should -Be "input:analog-stereo"
        $card.Profiles[0].DisplayName | Should -Be "Analog Stereo Input"
        $card.Profiles[0].SinkCount | Should -Be 0
        $card.Profiles[0].SourceCount | Should -Be 1
        $card.Profiles[0].Priority | Should -Be 65
        $card.Profiles[0].Available | Should -BeTrue

        $card.Profiles[1].SymbolicName | Should -Be "input:iec958-stereo"
        $card.Profiles[1].DisplayName | Should -Be "Digital Stereo (IEC958) Input"
        $card.Profiles[1].SinkCount | Should -Be 0
        $card.Profiles[1].SourceCount | Should -Be 1
        $card.Profiles[1].Priority | Should -Be 55
        $card.Profiles[1].Available | Should -BeFalse

        $card.Profiles[8].SymbolicName | Should -Be "off"
        $card.Profiles[8].DisplayName | Should -Be "Off"
        $card.Profiles[8].SinkCount | Should -Be 0
        $card.Profiles[8].SourceCount | Should -Be 0
        $card.Profiles[8].Priority | Should -Be 0
        $card.Profiles[8].Available | Should -BeTrue
    }
    
    It 'Populates ports correctly' {
        $card = Get-PACard -Name "alsa_card.pci-0000_2d_00.1"

        $card.Ports.Count | Should -Be 7

        $card.Ports[0].SymbolicName | Should -Be "hdmi-output-0"
        $card.Ports[0].DisplayName | Should -Be "HDMI / DisplayPort"
        $card.Ports[0].ProductName | Should -Be "ASUS MG278"
        $card.Ports[0].Available | Should -BeTrue
        $card.Ports[0].ProfileNames | Should -Be @("output:hdmi-stereo", "output:hdmi-surround", "output:hdmi-surround71")

        $card.Ports[1].SymbolicName | Should -Be "hdmi-output-1"
        $card.Ports[1].DisplayName | Should -Be "HDMI / DisplayPort 2"
        $card.Ports[1].ProductName | Should -BeNullOrEmpty
        $card.Ports[1].Available | Should -BeFalse
        $card.Ports[1].ProfileNames | Should -Be @("output:hdmi-stereo-extra1", "output:hdmi-surround-extra1", "output:hdmi-surround71-extra1")

        $card.Ports[6].SymbolicName | Should -Be "hdmi-output-6"
        $card.Ports[6].DisplayName | Should -Be "HDMI / DisplayPort 7"
        $card.Ports[6].ProductName | Should -BeNullOrEmpty
        $card.Ports[6].Available | Should -BeFalse
        $card.Ports[6].ProfileNames | Should -Be @("output:hdmi-stereo-extra6", "output:hdmi-surround-extra6", "output:hdmi-surround71-extra6")
    }

    It 'Populates the active profile correctly' {
        $card = Get-PACard -Name "alsa_card.usb-FiiO_DigiHug_USB_Audio-01"

        $card.ActiveProfile.SymbolicName | Should -Be "output:iec958-stereo"
    }
}

Describe 'Set-PACardProfile' {
    BeforeAll {
        $cardName = 'card1'
        $profileName = 'profile1'
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUserDeclaredVarsMoreThanAssignments', '')]
        $paCard = [PulseAudioCard] @{ Name = $cardName }
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUserDeclaredVarsMoreThanAssignments', '')]
        $paProfile = [PulseAudioProfile] @{ SymbolicName = $profileName }

        InModuleScope PoshPulseAudio {
            Mock pactl {} -ParameterFilter { $args[0] -eq "set-card-profile" }
        }
    }

    It 'Sets the active profile using names' {
        Set-PACardProfile -PACard $cardName -PAProfile $profileName 

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $cardName -and $args[2] -eq $profileName }
    }

    It 'Sets the active profile using objects' {
        Set-PACardProfile -PACard $paCard -PAProfile $paProfile 

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $cardName -and $args[2] -eq $profileName }
    }

    It 'Sets the active profile using a card from the pipeline' {
        $paCard | Set-PACardProfile -PAProfile $paProfile 

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $cardName -and $args[2] -eq $profileName }
    }

    It 'Throws an error when pactl outputs error message' {
        InModuleScope PoshPulseAudio {
            Mock pactl { "Failure: No such entity" } -ParameterFilter { $args[0] -eq "set-card-profile" }
        }

        { Set-PACardProfile -PACard $paCard -PAProfile $paProfile } |
            Should -Throw "Could not set profile for $cardName to $profileName`: Failure: No such entity"
    }
}

Describe 'Get-PASink' {
    BeforeAll {
        InModuleScope PoshPulseAudio {
            Mock pactl {
                $Global:testdata["sinks"]
            } -ParameterFilter { ($args -join " ") -eq "list sinks" }
        }
    }

    It 'Gets all sinks' {
        $sinks = Get-PASink
        
        $sinks.Count | Should -Be 2
        $sinks[0].Index | Should -Be 1
        $sinks[0].Name | Should -Be "alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo"
        $sinks[0].Description | Should -Be "Fiio E10 Digital Stereo (IEC958)"
        $sinks[1].Index | Should -Be 26
        $sinks[1].Name | Should -Be "alsa_output.pci-0000_2d_00.1.hdmi-stereo-extra2"
        $sinks[1].Description | Should -Be "TU104 HD Audio Controller Digital Stereo (HDMI 3)"
    }

    It 'Gets a pulse audio sink by name' {
        $sink = Get-PASink -Name "alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo"
        
        $sink.Index | Should -Be 1
        $sink.Name | Should -Be "alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo"
    }

    It 'Returns empty when named sink is not found' {
        $sink = Get-PASink -Name "alsa_output.usb-FiiO_DigiHug_USB_Audio-01.DNE"

        $sink | Should -BeNullOrEmpty
    }

    It 'Gets a pulse audio sink by wildcard name match' {
        $sinks = Get-PASink -Name "alsa_output.usb-FiiO_DigiHug_USB_Audio-01.*"
        
        $sinks.Count | Should -Be 1
        $sinks[0].Name | Should -Be "alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo"
        $sinks[0].Index | Should -Be 1
    }

    It 'Returns multiple sinks when multiple matches are found' {
        $sinks = Get-PASink -Name "alsa_output.*"
        
        $sinks.Count | Should -Be 2
        $sinks[0].Index | Should -Be 1
        $sinks[1].Index | Should -Be 26
    }

    It 'Returns empty when no matching sinks are found' {
        $sinks = Get-PASink -Name "alsa_output.DNE*"

        $sinks.Count | Should -Be 0
    }
}

Describe 'GetPASinkInput' {
    BeforeAll {
        InModuleScope PoshPulseAudio {
            Mock pactl {
                $Global:testdata["sinkinputs"]
            } -ParameterFilter { ($args -join " ") -eq "list sink-inputs" }
        }
    }

    It 'Gets all sink inputs' {
        $sinks = Get-PASinkInput
        
        $sinks.Count | Should -Be 3
        $sinks[0].Index | Should -Be 24
        $sinks[0].ApplicationName | Should -Be "speech-dispatcher-espeak-ng"
        $sinks[0].BinaryName | Should -Be "sd_espeak-ng"
        $sinks[0].ProcessId | Should -Be 15245
        $sinks[1].Index | Should -Be 25
        $sinks[1].ApplicationName | Should -Be "speech-dispatcher-dummy"
        $sinks[1].BinaryName | Should -Be "sd_dummy"
        $sinks[1].ProcessId | Should -Be 15251
        $sinks[2].Index | Should -Be 267
        $sinks[2].ApplicationName | Should -Be "WEBRTC VoiceEngine"
        $sinks[2].BinaryName | Should -Be "Discord"
        $sinks[2].ProcessId | Should -Be 5307
    }
}

Describe 'GetPASource' {
    BeforeAll {
        InModuleScope PoshPulseAudio {
            Mock pactl {
                $Global:testdata["sources"]
            } -ParameterFilter { ($args -join " ") -eq "list sources" }
        }
    }

    It 'Gets all sources' {
        $sources = Get-PASource
        
        $sources.Count | Should -Be 3
        $sources[0].Index | Should -Be 1
        $sources[0].Name | Should -Be "alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo.monitor"
        $sources[0].Description | Should -Be "Monitor of Fiio E10 Digital Stereo (IEC958)"
        $sources[1].Index | Should -Be 2
        $sources[1].Name | Should -Be "alsa_input.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.mono-fallback"
        $sources[1].Description | Should -Be "Audio Adapter (Unitek Y-247A) Mono"
        $sources[2].Index | Should -Be 28
        $sources[2].Name | Should -Be "alsa_output.pci-0000_2d_00.1.hdmi-stereo-extra2.monitor"
        $sources[2].Description | Should -Be "Monitor of TU104 HD Audio Controller Digital Stereo (HDMI 3)"
    }

    It 'Gets a pulse audio source by name' {
        $source = Get-PASource -Name "alsa_input.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.mono-fallback"
        
        $source.Index | Should -Be 2
        $source.Name | Should -Be "alsa_input.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.mono-fallback"
    }

    It 'Returns empty when named source is not found' {
        $source = Get-PASource -Name "alsa_input.usb-C-Media_Electronics_Inc.DNE"

        $source | Should -BeNullOrEmpty
    }

    It 'Gets a pulse audio source by wildcard name match' {
        $sources = Get-PASource -Name "alsa_output.usb-FiiO_*"
        
        $sources.Count | Should -Be 1
        $sources[0].Index | Should -Be 1
        $sources[0].Name | Should -Be "alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo.monitor"
    }

    It 'Returns multiple sources when multiple matches are found' {
        $sources = Get-PASource -Name "alsa_output.*"
        
        $sources.Count | Should -Be 2
        $sources[0].Index | Should -Be 1
        $sources[1].Index | Should -Be 28
    }

    It 'Returns empty when no matching sources are found' {
        $sources = Get-PASource -Name "alsa_output.DNE*"

        $sources.Count | Should -Be 0
    }
}

Describe 'GetPASourceOutput' {
    BeforeAll {
        InModuleScope PoshPulseAudio {
            Mock pactl {
                $Global:testdata["sourceoutputs"]
            } -ParameterFilter { ($args -join " ") -eq "list source-outputs" }
        }
    }

    It 'Gets all source outputs' {
        $sources = Get-PASourceOutput
        
        $sources.Count | Should -Be 1
        $sources[0].Index | Should -Be 13
        $sources[0].ApplicationName | Should -Be "WEBRTC VoiceEngine"
        $sources[0].BinaryName | Should -Be "Discord"
        $sources[0].ProcessId | Should -Be 5307
    }
}

Describe 'SetPAInputSink' {
    BeforeAll {
        $sinkName = 'sink1'
        $inputIndex = 2
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUserDeclaredVarsMoreThanAssignments', '')]
        $paSink = [PulseAudioSink] @{ Name = $sinkName }
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUserDeclaredVarsMoreThanAssignments', '')]
        $paSinkInput = [PulseAudioSinkInput] @{ Index = $inputIndex }

        InModuleScope PoshPulseAudio {
            Mock pactl {} -ParameterFilter { $args[0] -eq "move-sink-input" }
        }
    }

    It 'Moves the input to the sink using primitives' {
        Set-PAInputSink -PASink $sinkName -PAInput $inputIndex 

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $inputIndex -and $args[2] -eq $sinkName }
    }

    It 'Moves the input to the sink using objects' {
        Set-PAInputSink -PASink $paSink -PAInput $paSinkInput 

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $inputIndex -and $args[2] -eq $sinkName }
    }

    It 'Moves the input using an input from the pipeline' {
        $paSinkInput | Set-PAInputSink -PASink $paSink 

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $inputIndex -and $args[2] -eq $sinkName }
    }

    It 'Throws an error when pactl outputs error message' {
        InModuleScope PoshPulseAudio {
            Mock pactl { "Failure: No such entity" } -ParameterFilter { $args[0] -eq "move-sink-input" }
        }

        { Set-PAInputSink -PASink $paSink -PAInput $paSinkInput } |
            Should -Throw "Could not move input $inputIndex to $sinkName`: Failure: No such entity"
    }
}

Describe 'SetDefaultPASink' {
    BeforeAll {
        $sinkName = 'sink1'
        [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUserDeclaredVarsMoreThanAssignments', '')]
        $paSink = [PulseAudioSink] @{ Name = $sinkName }

        InModuleScope PoshPulseAudio {
            Mock pactl {} -ParameterFilter { $args[0] -eq "set-default-sink" }
        }
    }

    It 'Sets the default sink by name' {
        Set-DefaultPASink -PASink $sinkName

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $sinkName }
    }

    It 'Sets the default sink using objects' {
        Set-DefaultPASink -PASink $paSink

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $sinkName }
    }

    It 'Sets the default sink from the pipeline' {
        $paSink | Set-DefaultPASink

        Should -Invoke pactl -ModuleName PoshPulseAudio -Times 1 -ParameterFilter { $args[1] -eq $sinkName }
    }

    It 'Throws an error when pactl outputs error message' {
        InModuleScope PoshPulseAudio {
            Mock pactl { "Failure: No such entity" } -ParameterFilter { $args[0] -eq "set-default-sink" }
        }

        { Set-DefaultPASink -PASink $paSink } |
            Should -Throw "Could not set default sink to $sinkName`: Failure: No such entity"
    }
}