PSTestX.ps1

<#PSScriptInfo
 
.VERSION 1.0.20240619
 
.GUID a1626141-f76e-4301-8ec3-d03f6bfd4d2f
 
.AUTHOR Jimurrito
 
.COMPANYNAME Virtrillo Software Solutions
 
.COPYRIGHT (c) 2024 Jimurrito. All rights reserved.
 
.TAGS Testing Framework Unit-testing Assertion-testing Assertion cmdlet-testing function-testing
 
.LICENSEURI https://www.gnu.org/licenses/gpl-3.0.en.html
 
.PROJECTURI https://github.com/jimurrito/PSTest
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES PSTestLib
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES https://github.com/jimurrito/PSTest
 
#>



<#
.SYNOPSIS
    A script to run tests on PowerShell modules.
 
.DESCRIPTION
    This script imports the modules from a specified path, runs tests on functions that have a specific attribute, and outputs the results.
 
.PARAMETER TestPath
    The path to the directory containing the test modules. The script will attempt to test all modules in this directory. Only functions that have the PSTest() attribute will be evaluated.
 
.PARAMETER FullDump
    A switch value indicating whether to output full test results from the test runs.
 
.PARAMETER TestExtensions
    The file extensions of the modules to be tested.
 
.EXAMPLE
    PS C:\path\to\PSTest> .\PSTestX.ps1 -TestPath ".\Tests\" -FullDump $true
     
    or if installed
 
    PSTestX -TestPath ".\Tests\" -FullDump $true
 
.OUTPUTS
    [ResultType] class that can be found in PSTestLib
#>

#
#Requires -modules PSTestLib
#
#
param(
    # path to the test modules
    # will attempt to test all modules in the dir
    # Only functions that have the PSTest() attribute will be evaluated
    [string]$TestPath = "$PWD",
    [switch]$FullDump,
    [string]$TestExtensions = "*.psm1"
)
# using module PSTestLib
try { . ([scriptblock]::create("using module PSTestLib")) } catch { . ([scriptblock]::create("using module .\PSTestLib\PSTestLib.psd1")) }
#
#
# generate stop watch for execution timing
$Timer = [System.Diagnostics.Stopwatch]::StartNew()
#
#
# get powershell modules in the path
$Modules2Test = Get-ChildItem -Path $TestPath -Filter $TestExtensions -Verbose
#
#
#
# Ran for each module, in an isolated powershell session
$TestBlock = {
    #
    #Requires -modules PSTestLib
    #
    param(
        [string]$testModPath,
        [string]$TestAtt
    )
    # Import testlib obj
    try { . ([scriptblock]::create("using module PSTestLib")) } catch { . ([scriptblock]::create("using module .\PSTestLib\PSTestLib.psd1")) }
    #
    # Import module that needs to be tested
    Import-Module $testModPath
    # filters all the commands that contain the test class attribute [PSTest()]
    $funcs = get-module | Foreach-Object { 
        Get-Command -Module $_ | Where-Object { $_.ScriptBlock.Attributes.TypeId.name -eq $TestAtt } 
    }
    #
    # Iterate all in-scope functions - testing each one
    # Full single module output - reps all tests in a single module file - use `$ModuleResults`
    return $funcs | ForEach-Object {
        #
        $func = $_
        # get attributes - filters out attributes from other sources
        $AttVals = $func.ScriptBlock.Attributes | where-object { $_.TypeId.name -eq $TestAtt }
        #
        # Each iteration of $AttVals is its own test.
        # Results from all test permuations of a single function
        return $AttVals | ForEach-Object { 
            #
            # input args remap - needed to splatter the array of variables
            $InputArgs = $_.IArgs;
            $Assertion = $_.Assert;
            $AssertVar = $_.AssertVar;
            #
            # Test Job return - represents a single test
            $PermuationResult = try {
                # invoke test
                $TestResult = & $func.Name @InputArgs
                # Copy to custom var for assert ($r by default)
                Invoke-Expression ($AssertVar + '= $TestResult')       
                # Run if block if test should be asserted
                if ($Assertion -and !(& $Assertion)) {
                    return [PSTestResult]::new(
                        [ResultType]::AssertionError, 
                        $testresult, 
                        $func.Name, 
                        $InputArgs
                    )
                }
                # Test should NOT be asserted
                else {
                    return [PSTestResult]::new(
                        $testresult, 
                        $func.Name, 
                        $InputArgs
                    )
                }
            }
            # Job failed the test
            catch {
                return [PSTestResult]::new(
                    [ResultType]::ExceptionError, 
                    $_, 
                    $func.Name, 
                    $InputArgs
                )
            }
            #
            #
            # Test Job return - represents a single test - return on try/catch does not work in PS
            return $PermuationResult
        }
    }
    # end of scriptblock
}
#
# Execute test(s)
$FinalOutput = $Modules2Test | ForEach-Object { (pwsh -c $TestBlock -args @($_, "PSTestAttribute")) }
#
# [FOR TESTING ONLY - Runs in current session instread of new one(s).]
# $FinalOutput = $Modules2Test | ForEach-Object { (& $TestBlock $_ "PSTestAttribute") }
#
#
# [Stat output]
#
# Stop the stopwatch; capture output.
$Timer.Stop(); $Runtime = $Timer.Elapsed
#
# Count number that were successful
$SuccCount = ($FinalOutput | Where-Object { $_.ResultType -eq [ResultType]::Success }).Count
$FailCount = $FinalOutput.Count - $SuccCount
#
# verbose output
Write-Host "`nSuccess: "-NoNewline
Write-Host "$SuccCount" -ForegroundColor Green -NoNewline
Write-Host " | "  -NoNewline
Write-Host "Failure: " -NoNewline
Write-Host "$FailCount" -ForegroundColor Red -NoNewline
Write-Host " | "  -NoNewline
Write-Host ("Total: {0} | Runtime: ({1})s ({2})ms`n" -f 
    $FinalOutput.Count, $Runtime.Seconds, $Runtime.Milliseconds ) -NoNewline
#
# Output of ALL tests ran for all modules
if ($FullDump) { Write-Output $FinalOutput }