PSTestX.ps1

<#PSScriptInfo
 
.VERSION 1.2024061801
 
.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 TestAttributeName
    The name of the attribute that the script looks for in the functions to be tested.
 
.PARAMETER FullDump
    A boolean value indicating whether to output all tests run for all modules.
 
.PARAMETER TestExtensions
    The file extensions of the modules to be tested.
 
.EXAMPLE
    PS C:\> .\PSTestX.ps1 -TestPath ".\Tests\" -TestAttributeName "PSTestAttribute" -FullDump $false -TestExtensions "*.psm1"
 
.OUTPUTS
    [ResultType] class that can be found in PSTestLib
#>


#Requires -Modules PSTestLib
using module 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
    [Parameter(Mandatory = $true)]
    [string]$TestPath,
    [string]$TestAttributeName = "PSTestAttribute",
    [bool]$FullDump = $false,
    [string]$TestExtensions = "*.psm1"
)
#
#
# 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
    . ([scriptblock]::create("using module PSTestLib"))
    # 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;
            #
            # Test Job return - represents a single test
            $PermuationResult = try {
                # invoke test
                $TestResult = & $func.Name @InputArgs
                # Run if block if test should be asserted
                if ($Assertion -and $TestResult -ne $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 @($_, $TestAttributeName)) }
#
# [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 }