functions/Test-Expression.ps1



#an internal function for the actual testing
Function _TestMe {
    [cmdletbinding(DefaultParameterSetName = "Interval")]
    Param(
        [scriptblock]$Expression,
        [object[]]$ArgumentList,
        [ValidateScript( { $_ -ge 1 })]
        [int]$Count = 1,
        [Parameter(ParameterSetName = "Interval")]
        [ValidateRange(0, 60)]
        [double]$Interval = .5,
        [Parameter(ParameterSetName = "Random", Mandatory)]
        [Alias("min")]
        [double]$RandomMinimum,
        [Parameter(ParameterSetName = "Random", Mandatory)]
        [Alias("max")]
        [double]$RandomMaximum,
        [switch]$IncludeExpression
    )

    $TestData = 1..$count | ForEach-Object -begin {
        <#
          PowerShell doesn't seem to like passing a scriptblock as an
          argument when using Invoke-Command. It appears to pass it as
          a string so I'm recreating it as a scriptblock here.
         #>

        $script:TestBlock = [scriptblock]::Create($Expression)

    } -process {
        #invoke the scriptblock with any arguments and measure
        Measure-Command -Expression { $($script:TestBlock).Invoke(@($argumentlist)) } -OutVariable +out

        #} -outvariable +out
        #pause to mitigate any caching effects
        if ($RandomMinimum -AND $RandomMaximum) {
            $sleep = Get-Random -Minimum ($RandomMinimum * 1000) -Maximum ($RandomMaximum * 1000)
            $TestInterval = "Random"
        }
        else {
            $Sleep = ($Interval * 1000)
            $TestInterval = $Sleep
        }

        Start-Sleep -Milliseconds $sleep
    }

    $TestResults = $TestData |
    Measure-Object -Property TotalMilliseconds -Average -Maximum -Minimum |
    Select-Object -Property @{Name = "Tests"; Expression = { $_.Count } },
    @{Name = "TestInterval"; Expression = { $TestInterval } },
    @{Name = "AverageMS"; Expression = { $_.Average } },
    @{Name = "MinimumMS"; Expression = { $_.Minimum } },
    @{Name = "MaximumMS"; Expression = { $_.Maximum } },
    @{Name = "MedianMS"; Expression = {
            #sort the values to calculate the median and trimmed values
            $sort = $out.TotalMilliseconds | Sort-Object

            #test if there are an even or odd number of elements
            if ( ($sort.count) % 2) {
                #odd number
                #subtract 1 because arrays start counting at 0
                $sort[(($sort.count - 1) / 2) -as [int]]
            }
            else {
                #even number
                #get middle two numbers and their average
                ($sort[($sort.count / 2)] + $sort[$sort.count / 2 + 1]) / 2
            }
        }
    },
    @{Name = "TrimmedMS"; Expression = {
            #values must be sorted in ascending order
            $data = $out.TotalMilliseconds | Sort-Object
            #select elements from the second to next to last
            ($data[1..($data.count - 2)] | Measure-Object -Average).Average

        }
    }

    #add metadata
    $OS = Get-CimInstance -ClassName Win32_OperatingSystem
    $TestResults | Add-Member -MemberType NoteProperty -Name PSVersion -Value $PSVersionTable.PSVersion.ToString()
    $TestResults | Add-Member -MemberType NoteProperty -Name OS -Value $OS.caption

    if ($IncludeExpression) {
        Write-Verbose "Adding expression to output"
        $TestResults | Add-Member -MemberType NoteProperty -Name Expression -Value $Expression
        $TestResults | Add-Member -MemberType NoteProperty -Name Arguments -Value $ArgumentList
    }

    Write-Verbose "Inserting a new type name"
    $TestResults.PSObject.TypeNames.insert(0, "my.TestResult")

    #write the result to the pipeline
    $testResults
} #_TestMe function

#exposed functions
Function Test-Expression {

    [cmdletbinding(DefaultParameterSetName = "Interval")]
    [alias("tex")]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            HelpMessage = "Enter a scriptblock to test",
            ValueFromPipeline
        )]
        [Alias("sb")]
        [scriptblock]$Expression,

        [object[]]$ArgumentList,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateScript( { $_ -ge 1 })]
        [int]$Count = 1,

        [Parameter(
            ParameterSetName = "Interval",
            ValueFromPipelineByPropertyName)]
        [ValidateRange(0, 60)]
        [Alias("sleep")]
        [double]$Interval = .5,

        [Parameter(
            ParameterSetName = "Random",
            Mandatory
        )]
        [Alias("min")]
        [double]$RandomMinimum,

        [Parameter(
            ParameterSetName = "Random",
            Mandatory
        )]
        [Alias("max")]
        [double]$RandomMaximum,

        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias("ie")]
        [switch]$IncludeExpression,

        [switch]$AsJob

    )

    Write-Verbose "Starting: $($MyInvocation.MyCommand)"
    Write-Verbose ($PSBoundParameters | Out-String)
    Write-Verbose "Measuring expression:"
    Write-Verbose ($Expression | Out-String)
    if ($ArgumentList) {
        Write-Verbose "Arguments: $($ArgumentList -join ",")"
    }

    if ($PSCmdlet.ParameterSetName -eq 'Interval') {
        Write-Verbose "$Count time(s) with a sleep interval of $interval seconds."
    }
    else {
        Write-Verbose "$Count time(s) with a random sleep interval between $RandomMinimum seconds and $RandomMaximum seconds."
    }

    If ($AsJob) {
        Write-Verbose "Running as a background job"
        [void]$PSBoundParameters.remove("AsJob")
        Start-Job -ScriptBlock {
            Param([hashtable]$TestParams)

            <#
          PowerShell doesn't seem to like passing a scriptblock as an
          argument when using Invoke-Command. It appears to pass it as
          a string so I'm recreating it as a scriptblock here.
        #>


            $expression = [scriptblock]::Create($TestParams.Expression)
            $TestParams.Expression = $Expression
            Test-Expression @TestParams
        } -ArgumentList @($PSBoundParameters) -InitializationScript { Import-Module PSScriptTools}

    }
    else {
        [void]$PSBoundParameters.remove("AsJob")
        _TestMe @PSBoundParameters
    }

    Write-Verbose "Ending: $($MyInvocation.MyCommand)"

} #end function

Function Test-ExpressionForm {
    [cmdletbinding()]
    [alias("texf")]
    Param()

    if ((Test-IsPSWindows)) {

        Add-Type -AssemblyName PresentationFramework
        Add-Type -assemblyName PresentationCore

        [xml]$xaml = Get-Content $PSScriptRoot\form.xaml

        $reader = New-Object System.Xml.XmlNodeReader $xaml
        $form = [Windows.Markup.XamlReader]::Load($reader)

        $sb = $form.FindName("txtScriptBlock")
        $count = $form.FindName("txtCount")
        $results = $form.FindName("tbResults")
        $slider = $form.FindName("sliderStatic")
        $radioStatic = $form.FindName("radioStatic")
        $radioRandom = $form.FindName("radioRandom")
        $min = $form.FindName("txtMin")
        $Max = $form.FindName("txtMax")
        $argumentList = $form.FindName("txtArguments")
        $run = $form.FindName("btnRun")
        $quit = $form.FindName("btnQuit")

        #defaults
        $min.Text = 1
        $max.text = 5
        $min.IsEnabled = $False
        $max.IsEnabled = $false
        $slider.IsEnabled = $True

        $radioStatic.add_Checked( {
                $min.IsEnabled = $False
                $max.IsEnabled = $false
                $slider.IsEnabled = $True
            })

        $radioRandom.Add_checked( {
                $min.IsEnabled = $True
                $max.IsEnabled = $True
                $slider.IsEnabled = $False
            })

        $quit.add_click( { $form.close() })

        Function _refresh {
            #this is a function to refresh a UI element
            Param($element)

            $element.Dispatcher.invoke("render", [action] {})
            [System.Threading.Thread]::Sleep(50)

        }

        $run.add_click( {

                $results.text = "Testing...please wait"
                _refresh $results

                #uncomment for troubleshooting
                #write-host "running" -ForegroundColor green

                $form.Dispatcher.invoke([action] {
                        if ($sb.Text -notmatch "\w") {
                            Write-Warning "You must enter something to test!"
                            Return
                        }

                        $params = @{
                            Expression        = [scriptblock]::Create($sb.text)
                            Count             = $count.text -as [int]
                            IncludeExpression = $True
                        }

                        If ($argumentList.text) {
                            $params.Add("ArgumentList", ($argumentList.Text -split ","))
                        }

                        if ($radioStatic.IsChecked) {
                            $interval = [math]::round($slider.value, 1)
                            $params.Add("interval", $interval)
                        }
                        else {
                            [double]$minimum = [math]::Round($min.Text, 1)
                            [double]$maximum = [math]::Round($max.text, 1)
                            $params.Add("RandomMinimum", $minimum)
                            $params.Add("RandomMaximum", $maximum)
                        }

                        $form.Cursor = [System.Windows.Input.Cursors]::Wait
                        #uncomment for troubleshooting
                        #$params | out-string | write-host -ForegroundColor cyan

                        $script:out = Test-Expression @params -ErrorVariable ev
                        if ($script:out) {
                            $data = ($script:out | Select-Object -property * -exclude OS, Expression, Arguments | Out-String).Trim()
                        }
                        else {
                            $data = $ev.exception[0].message
                        }
                        $results.text = $data
                        $form.Cursor = [System.Windows.Input.Cursors]::Default
                    })
            })

        [void]$sb.Focus()
        [void]$form.ShowDialog()

        #write the current results to the pipeline after the form is closed.
        $script:out
    }
    else {
        Write-Warning "This command requires a Windows platform that supports WPF."
    }
}