Start-Jojoba.ps1

<#
 
.SYNOPSIS
Parallel processing with test case output and Jenkins integration.
 
.DESCRIPTION
For a Jojoba template function this will be the main call in the process {} block. All processing should occur within here.
 
.PARAMETER ScriptBlock
The test to carry out. It must use $InputObject or $_.
 
.INPUTS
All inputs aside from the ScriptBlock are taken from the calling function.
    $JojobaBatch
    $JojobaCallback (optional, for writing events elsewhere)
    $JojobaJenkins (optional, for forcing a write of XML)
    $JojobaThrottle (required for batch runs, optional for testing)
    $JojobaSuite (optional)
 
.OUTPUTS
A test case object.
 
#>


function Start-Jojoba {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ScriptBlock] $ScriptBlock
    )

    begin {
    }

    process {
        # Gather information about the caller
        $jojobaModuleName = $PSCmdlet.GetVariableValue("MyInvocation").MyCommand.ModuleName
        $jojobaClassName = $PSCmdlet.GetVariableValue("MyInvocation").MyCommand.Name
        $jojobaName = $PSCmdlet.GetVariableValue("InputObject")

        if ($jojobaClassNameFolder = $PSCmdlet.GetVariableValue("PSCommandPath")) {
            $jojobaClassNameFolder = Split-Path -Leaf (Split-Path -Parent $jojobaClassNameFolder)
        } 
    
        # Inherit Jojoba parameters from the caller
        $JojobaBatch = $PSCmdlet.GetVariableValue("JojobaBatch")
        if (!($JojobaCallback = $PSCmdlet.GetVariableValue("JojobaCallback"))) {
            $JojobaCallback = "Write-JojobaCallback"
        }
        $JojobaJenkins = $PSCmdlet.GetVariableValue("JojobaJenkins")
        if (!($JojobaSuite = $PSCmdlet.GetVariableValue("JojobaSuite"))) {
            if ($JojobaSuite = $jojobaModuleName) {
                if ($jojobaClassNameFolder -and $jojobaClassNameFolder -ne $jojobaModuleName) {
                    $JojobaSuite += "\$jojobaClassNameFolder"
                }
            } else {
                $JojobaSuite = "(Root)"
            }
        }
        $JojobaThrottle = $PSCmdlet.GetVariableValue("JojobaThrottle")
        # You'd almost always specify JojobaThrottle. If you didn't, well, it's allowed, but
        if ($JojobaThrottle -and !$JojobaBatch) {
            Write-Error "When running in PoshRSJob mode the caller must have a unique `$JojobaBatch per pipeline"
        }
        if (!$JojobaThrottle -and $JojobaJenkins) {
            Write-Error "When running in direct mode the caller cannot use `$JojojobaJenkins"
        }

        if (!$JojobaThrottle) {
            #region Direct run
            Write-Verbose "Starting inside thread for $JojobaName"
            
            # Fill out the base test case, named after parts of the original caller
            $jojobaTestCase = [PSCustomObject] @{
                UserName  = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
                Suite     = $JojobaSuite
                Timestamp = Get-Date
                Time      = 0
                ClassName = $JojobaClassName
                Name      = $JojobaName
                Result    = "Pass"
                Message   = New-Object Collections.ArrayList
                Data      = New-Object Collections.ArrayList
            }
            $jojobaAbort = [PSCustomObject] @{
                Message = New-Object Collections.ArrayList
            }
            
            $jojobaMessages = try {
                &$ScriptBlock *>&1
            } catch {
                # Handle an uncaught stop as a test block failure. This saves
                # having to write test code for everything if the exception is
                # self explanatory
                Write-JojobaFail $_.ToString()
                Resolve-Error $_ -AsString
            }

            foreach ($jojobaMessage in ($jojobaMessages | Where-Object { $null -ne $_ })) {
                if ($jojobaMessage -is [string]) {
                    [void] $jojobaTestCase.Data.Add($jojobaMessage)
                } elseif ($jojobaMessage.GetType().FullName -eq "System.Management.Automation.InformationRecord") {
                    # Used in PS5 to capture Write-Host output
                    if ($jojobaMessage.Tags.Contains("PSHOST")) {
                        [void] $jojobaTestCase.Data.Add($jojobaMessage)      
                    } else {
                        [void] $jojobaTestCase.Data.Add($jojobaMessage)
                    }
                } elseif ($jojobaMessage -is [System.Management.Automation.VerboseRecord]) {
                    [void] $jojobaTestCase.Data.Add("VERBOSE: $jojobaMessage")
                } elseif ($jojobaMessage -is [System.Management.Automation.WarningRecord]) {
                    [void] $jojobaTestCase.Data.Add("WARNING: $jojobaMessage")
                } elseif ($jojobaMessage -is [System.Management.Automation.ErrorRecord]) {
                    # Exceptions also get wrapped in an ErrorRecord
                    [void] $jojobaTestCase.Data.Add((Resolve-Error $jojobaMessage -AsString))
                } else {
                    [void] $jojobaTestCase.Data.Add(($jojobaMessage | Format-List | Out-String))
                }
            }

            # Calculate other useful information for the test case for use by Jenkins
            $jojobaTestCase.Time = ((Get-Date) - $jojobaTestCase.Timestamp).TotalSeconds
            
            # Write out the test case after getting rid of {} marks
            $jojobaTestCase.Message = $jojobaTestCase.Message -join [Environment]::NewLine
            $jojobaTestCase.Data = $jojobaTestCase.Data -join [Environment]::NewLine

            if (!$jojobaAbort.Message) { 
                # If the calling function has a Write-Jojoba then send them a copy of the test. If this fails,
                # it also makes the test fail and which is at least output somewhere.
                if ($jojobaCallbackReference = Get-Command -Module $JojobaModuleName | Where-Object { $_.Name -eq $JojobaCallback }) {
                    try {
                        &$jojobaCallbackReference $jojobaTestCase
                    } catch {
                        $jojobaTestCase.Message += [Environment]::NewLine + $_.ToString()
                        $jojobaTestCase.Data += [Environment]::NewLine + (Resolve-Error $_ -AsString)
                    }
                }

                $jojobaTestCase | Select-Object UserName, Suite, Timestamp, Time, ClassName, Name, Result, Message, Data
            } else {
                Write-Error ($jojobaAbort.Message -join [Environment]::NewLine)
            }
            #endregion
        } else {
            #region Parallel run
            # These are arguments which will be splatted for use by PoshRSJob
            $jobArguments = @{
                Name            = $JojobaName
                Throttle        = $JojobaThrottle
                Batch           = $JojobaBatch
                ModulesToImport = $JojobaModuleName
                FunctionsToLoad = if (!$jojobaModuleName) {
                    $JojobaClassName 
                } else {
                    $null 
                }
                ScriptBlock     = [scriptblock]::Create("`$_ | $($JojobaClassName) -JojobaThrottle 0")
                Verbose         = $VerbosePreference
            }

            # Add any extra switches and parameters to the scriptblock so they can be passed to the caller.
            # This can't handle complex objects - those should be piped in instead.
            # Some exceptions for Jojoba flags are included.
            $PSCmdlet.GetVariableValue("MyInvocation").BoundParameters.GetEnumerator() | ForEach-Object {
                if (@("InputObject", "JojobaBatch", "JojobaJenkins", "JojobaThrottle") -notcontains $_.Key) {
                    if ($_.Value -is [System.Management.Automation.SwitchParameter]) {
                        $jobArguments.ScriptBlock = [scriptblock]::Create("$($jobArguments.ScriptBlock) -$($_.Key):`$$($_.Value)")
                    } elseif ($_.Value -is [string]) {
                        $jobArguments.ScriptBlock = [scriptblock]::Create("$($jobArguments.ScriptBlock) -$($_.Key) '$($_.Value.Replace("'", "''"))'")
                    } else {
                        $jobArguments.ScriptBlock = [scriptblock]::Create("$($jobArguments.ScriptBlock) -$($_.Key) $($_.Value)")
                    }
                }
            }

            Write-Verbose "Scheduling $($JojobaName) batch $($JojobaBatch) throttle $($jobArguments.Throttle) modules $($jobArguments.ModulesToImport) functions $($jobArguments.FunctionsToLoad) script $($jobArguments.ScriptBlock)"
            # Here we can continue to pipe in a complex object, or, revert back to the InputObject, for simplicity
            $null = @(if ($PSCmdlet.GetVariableValue("_") -and $PSCmdlet.GetVariableValue("_") -isnot [string]) {
                    $PSCmdlet.GetVariableValue("_") 
                } else {
                    $PSCmdlet.GetVariableValue("InputObject") 
                }) | Start-RSJob @jobArguments
            #endregion
        }
    }

    end {
    }
}