Saturday, January 2, 2021

PowerShell: A Practical Example (Logging and Testing)

Overview

The vast majority of the PowerShell projects I undertake are in some partial stage of development. Most projects lack a standardized approach to logging and lack a standardized approach to testing. Rather than writing logging and testing infrastructure for each project, this post contains basic but comprensive implementation of logging and testing including modules (psm1 files) and corresponding test scripts (ps1 files)

  • QEDLogging.psm1: basic logging to file/console using standard formatting (tsv/cvs, Json, Xml)
  • QEDLoggingTest.ps1: unit tests for QEDLogging.psm1 that use the unit test framework provided by PsUnit.psm1
  • PsUnit.psm1: practical but basic unit test framework.
  • PsUnitTest.ps1: test code for PsUnit.psm1

 Source Code

QEDLogging.psm1

Import-Module '.\PsUnit.psm1'

Set-StrictMode -Version 3.0

enum LogLevel {
    Error
    Warn
    Info
    Verbose
}

enum LogItemIndex {
    Date    
    LogLevel
    Filename
    LineNumber
    Tags
    Message
}

class QEDLogCommon {
    hidden static [string] $tagException

    hidden static [char] $delimeterElement

    hidden static [char] $delimeterTag

    static QEDLogEntry() {
        [QEDLogEntry]::delimeterElement = "`t"
        [QEDLogEntry]::delimeterTag = ','
        [QEDLogEntry]::tagException = 'Exception'
    }

    static [string] GetTagException() {
        return [QEDLogCommon]::tagException
    }

    static [char] GetDelimeterElement() {
        return [QEDLogCommon]::delimeterElement
    }

    static [char] GetDelimeterTag() {
        return [QEDLogCommon]::delimeterTag
    }
}

class ErrorRecordExtension {
    hidden static [char] $messageSeparator

    hidden [System.Management.Automation.ErrorRecord] $errorRecord

    hidden [int] $exceptionHandledAtLineNumber 

    hidden [string] $exceptionHandledAtFilename 

    static ErrorRecordExtension() {
        [ErrorRecordExtension]::messageSeparator = ':'
    }

    ErrorRecordExtension() {
    }
    
    ErrorRecordExtension(
        [System.Management.Automation.ErrorRecord] $errorRecord,        
        [int] $exceptionHandledAtLineNumber,
        [string] $exceptionHandledAtFilename) {

        $this.exceptionHandledAtLineNumber = 
            $exceptionHandledAtLineNumber
        $this.exceptionHandledAtFilename = 
            $exceptionHandledAtFilename            
        $this.errorRecord = $errorRecord
    }

    static [char] GetMessageSeparator()  {
        return [ErrorRecordExtension]::messageSeparator        
    }

    static [char] SetMessageSeparator([char] $messageSeparator)  {
        [char] $originalValue = [ErrorRecordExtension]::messageSeparator
        
        [ErrorRecordExtension]::messageSeparator = $messageSeparator

        return $originalValue
    }    

    hidden static [string] Flatten([string] $text) {
        return $text.Replace("`n", '').Replace("`r", '')
    }

    hidden static [string] GetFilename([string] $fullPath) {
        if ([string]::IsNullOrEmpty($fullPath)) {
            return ''
        }

        return Split-Path $fullPath -Leaf
    }

    [int] GetHandledAtLineNumber() {
        return $this.exceptionHandledAtLineNumber
    }

    [string] GetHandledAtScriptName() {
        return [ErrorRecordExtension]::GetFilename(
                    $this.exceptionHandledAtFilename)
    }

    [string] GetInvocationLine() {
        return [ErrorRecordExtension]::Flatten(
                    $this.errorRecord.InvocationInfo.Line)
    }

    [int] GetInvocationScriptLineNumber() {
        return $this.errorRecord.InvocationInfo.ScriptLineNumber
    }

    [string] GetInvocationScriptName() {
        return [ErrorRecordExtension]::GetFilename(
                $this.errorRecord.InvocationInfo.ScriptName)
    }

    [string] GetInvocationPosition() {
        return [ErrorRecordExtension]::Flatten(
            $this.errorRecord.InvocationInfo.PositionMessage).
                Replace('~', '').Replace('+', '')
    }

    hidden static [string] GetStackTrace([Exception] $exception) {
        return [ErrorRecordExtension]::Flatten(
                    $exception.StackTrace) -replace '\s+', ' '
    }

    hidden static [string] GetException([Exception] $exception) {
        [string] $message = 
            "$($exception.Message)$([ErrorRecordExtension]::messageSeparator)" + 
            "$($exception.TargetSite)$([ErrorRecordExtension]::messageSeparator)" + 
            "$([ErrorRecordExtension]::GetStackTrace($exception))"

        if ($null -ne $exception.InnerException) {
            $message += 
                [ErrorRecordExtension]::messageSeparator +
                [ErrorRecordExtension]::GetException(
                         $exception.InnerException)
        }

        return $message
    }

    [string] GetException() {
        return [ErrorRecordExtension]::GetException(
                            $this.errorRecord.Exception)
    }

    [System.Management.Automation.ErrorRecord] GetErrorRecord() {
        return $this.errorRecord
    }
}

class QEDLogEntry {
    hidden [DateTime] $dateTime

    hidden [LogLevel] $logLevel

    hidden [string] $filename

    hidden [int] $lineNumber

    hidden [System.Collections.ObjectModel.ReadOnlyCollection[string]] $tags

    hidden [string] $message
    
    QEDLogEntry(
        [LogLevel] $logLevel,
        [string] $filename,
        [int] $lineNumber,
        [string] $message) {
        
        $this.Assign(
            [QEDLogEntry]::GetUtcNow(),
            $logLevel,
            $filename,
            $lineNumber,
            $null,
            $message)
    }

    QEDLogEntry(
        [LogLevel] $logLevel,
        [string] $filename,
        [int] $lineNumber,
        [string[]] $tags,
        [string] $message) {

        $this.Assign(
            [QEDLogEntry]::GetUtcNow(),
            $logLevel,
            $filename,
            $lineNumber,
            $tags,
            $message)                
    }

    QEDLogEntry([string] $logLine) {
        [string[]] $items = 
            $logLine -split [QEDLogCommon]::GetDelimeterElement()
          
        $this.Assign(
            ([DateTime]$items[[LogItemIndex]::Date]).ToUniversalTime(),
            [LogLevel]$items[[LogItemIndex]::LogLevel],
            $items[[LogItemIndex]::Filename],
            $items[[LogItemIndex]::LineNumber],
            $items[[LogItemIndex]::Tags] -split [QEDLogCommon]::GetDelimeterTag(),
            $items[[LogItemIndex]::Message])
    }    

    static [QEDLogEntry[]] Create(
        [LogLevel] $logLevel,
        [System.Management.Automation.ErrorRecord] $errorRecord,        
        [int] $exceptionHandledAtLineNumber,
        [string] $exceptionHandledAtFilename) {

        return [QEDLogEntry]::Create(
                    $logLevel, 
                    $errorRecord, 
                    $exceptionHandledAtFilename,
                    $exceptionHandledAtLineNumber,
                    [string[]]@())
    }

    static [QEDLogEntry[]] Create(
        [LogLevel] $logLevel,
        [System.Management.Automation.ErrorRecord] $errorRecord,        
        [string] $exceptionHandledAtFilename, 
        [int] $exceptionHandledAtLineNumber,
        [string[]] $tags) {

        [string[]] $localTags = $tags + [QEDLogCommon]::GetTagException()
        [DateTime] $utcNow = [QEDLogEntry]::GetUtcNow()
        [System.Collections.ObjectModel.ReadOnlyCollection[string]] $readOnlyTags = 
            [System.Collections.ObjectModel.ReadOnlyCollection[string]]::new($localTags.Clone())
        [ErrorRecordExtension] $errorRecordExtension = 
            [ErrorRecordExtension]::new($errorRecord, $exceptionHandledAtLineNumber, $exceptionHandledAtFilename)
        [QEDLogEntry] $exceptionLogEntry = [QEDLogEntry]::new(
                            $utcNow, 
                            $logLevel, 
                            $errorRecordExtension.GetHandledAtScriptName(), 
                            $errorRecordExtension.GetHandledAtLineNumber(), 
                            $readOnlyTags, 
                            $errorRecordExtension.GetException())
        [QEDLogEntry] $invocationLocationLogEntry = [QEDLogEntry]::new(
                            $utcNow, 
                            $logLevel, 
                            $errorRecordExtension.GetInvocationScriptName(), 
                            $errorRecordExtension.GetHandledAtLineNumber(), 
                            $readOnlyTags, 
                            $errorRecordExtension.GetInvocationPosition())

        return @($exceptionLogEntry, $invocationLocationLogEntry)
    }

    hidden [void] Assign(
        [DateTime] $date,
        [LogLevel] $logLevel,
        [string] $filename,
        [int] $lineNumber,
        [System.Collections.ObjectModel.ReadOnlyCollection[string]] $tags,        
        [string] $message) {

        $this.dateTime = $date
        $this.logLevel = $logLevel
        $this.lineNumber = $lineNumber
        $this.filename = $filename
        $this.message = $message
        if ($null -eq $tags) {
            $this.tags = [System.Collections.ObjectModel.ReadOnlyCollection[string]]::new([string[]]@())
        }

        else {
            $this.tags = $tags
        }
    }

    static [DateTime] GetUtcNow() {
        return (Get-Date).ToUniversalTime()
    }

    [DateTime] GetDateTime() {
        return $this.dateTime
    }

    [string] GetDateTimeText() {
        return $this.dateTime.ToString('o')
    }

    [LogLevel] GetLogLevel() {
        return $this.logLevel
    }

    [string] GetFilename() {
        return $this.filename            
    }

    [string] GetLineNumber() {
        return $this.lineNumber            
    }

    [System.Collections.ObjectModel.ReadOnlyCollection[string]] GetTags() {
        return $this.tags            
    }

    [string] GetMessage() {
        return $this.message            
    }

    [bool] IsError() {
        return [LogLevel]::Error -eq $this.logLevel
    }

    [bool] IsWarn() {
        return [LogLevel]::Warn -eq $this.logLevel
    }

    [bool] IsInfo() {
        return [LogLevel]::Info -eq $this.logLevel
    }

    [bool] IsVerbose() {
        return [LogLevel]::Verbose -eq $this.logLevel
    }

    [bool] IsException() {
        return $this.tags -contains [QEDLogCommon]::exceptionTagName
    }

    [bool] Contains([string] $tag) {
        return $this.tags -contains $tag
    }

    hidden [string] GetTagText() {
        if (0 -eq $this.tags.Count) {
            return ''
        }

        else {
            return $this.tags -join [QEDLogCommon]::GetDelimeterTag()
        }
    }

    [string] ToString() {
        return `
            "$($this.GetDateTimeText())$([QEDLogCommon]::GetDelimeterElement())" + 
                "$($this.logLevel)$([QEDLogCommon]::GetDelimeterElement())" + 
                "$($this.filename)$([QEDLogCommon]::GetDelimeterElement())" + 
                "$($this.lineNumber)$([QEDLogCommon]::GetDelimeterElement())" + 
                "$($this.GetTagText())$([QEDLogCommon]::GetDelimeterElement())" + 
                "$($this.message)"
    }
}

class QEDLog {
    hidden static [bool] $filenamePrefix

    hidden [bool] $consoleEnabled

    hidden [LogLevel] $logLevelConsole

    hidden [bool] $fileEnabled

    hidden [LogLevel] $logLevelFile

    hidden [string] $filename

    static QEDLog() {
        [QEDLog]::filenamePrefix = 'QED'
    }

    QEDLog(
        [LogLevel] $logLevelConsole, 
        [LogLevel] $logLevelFile, 
        [string] $filename) {

            $this.Assign(
                $logLevelConsole, 
                $logLevelFile, 
                $filename)
    }

    QEDLog([LogLevel] $logLevelConsole, [LogLevel] $logLevelFile) {
        $this.Assign(
            $logLevelConsole, 
            $logLevelFile, 
            [QEDLog]::CreateFilename())
    }

    hidden [void] Assign(
                    [LogLevel] $logLevelConsole, 
                    [LogLevel] $logLevelFile, 
                    [string] $filename) {

        $this.consoleEnabled = $true
        $this.fileEnabled = $true
        $this.logLevelConsole = $logLevelConsole
        $this.logLevelFile = $logLevelFile
        $this.filename = $filename
        New-Item -Path $this.filename -ItemType File | Out-Null
        Set-ItemProperty -Path $this.filename -Name IsReadOnly -Value $True        
    }    

    hidden static [string] CreaterRandomName() {
        return (Get-Date).ToUniversalTime().ToString('yyyyMMddhhmmssfff')
    }

    hidden static [string] CreateFilename() {
        return Join-Path `
            -Path $($Env:Temp) `
            -ChildPath "$([QEDLog]::filenamePrefix)$([QEDLog]::CreaterRandomName()).log"
    }

    [string] GetConsoleEnabled() {
        return $this.consoleEnabled
    }

    [bool] SetConsoleEnabled([bool] $enabled) {
        [bool] $originaValue = $this.consoleEnabled

        $this.consoleEnabled = $enabled

        return $originaValue
    }

    [string] GetFileEnabled() {
        return $this.fileEnabled
    }

    [bool] SetFileEnabled([bool] $enabled) {
        [bool] $originaValue = $this.fileEnabled

        $this.fileEnabled = $enabled
        
        return $originaValue
    }

    [string] GetFilename() {
        return $this.filename
    }

    [LogLevel] GetLevelConsole() {
        return $this.logLevelConsole
    }

    [LogLevel] SetLevelConole([LogLevel] $logLevel) {
        [LogLevel] $originalValue = $this.logLevelConsole

        $this.logLevelConsole = $logLevel

        return $originalValue
    }

    [LogLevel] GetLevelFile() {
        return $this.logLevelFile
    }

    [LogLevel] SetLevelFile([LogLevel] $logLevel) {
        [LogLevel] $originalValue = $this.logLevelFile

        $this.logLevelFile = $logLevel

        return $originalValue
    }

    [void] Write([QEDLogEntry] $logEntry) {
        if (!($this.fileEnabled -or $this.consoleEnabled)) {
            return
        }

        [LogLevel] $logLevel = $logEntry.GetLogLevel()

        if (($logLevel -lt $this.logLevelConsole) -and ($logLevel -lt $this.logLevelFile)) {
            return 
        }

        [string] $line = $logEntry.ToString() 

        if ($logLevel -le $this.logLevelConsole) {
            Write-Host $line
        }

        if ($logLevel -le $this.logLevelFile) {
            Add-Content -Path $this.filename -Value $line -Force
        }
    }
}

function Write-LogErrorRecord {
    Param(
        [Parameter(Mandatory=$true)]
        [QEDLog] $log,            
        [Parameter(Mandatory=$true)]
        [System.Management.Automation.ErrorRecord] $errorRecord,
        [string[]] $tags = @()
    )

    [QEDLogEntry[]] $logEntries = [QEDLogEntry]::Create(
        [LogLevel]::Error, 
        $errorRecord, 
        $($MyInvocation.ScriptName), 
        $($MyInvocation.ScriptLineNumber), 
        $tags)

    foreach ($logEntry in $logEntries) {
        $log.Write($logEntry)
    }
}

function Write-LogError {
    Param(
        [Parameter(Mandatory=$true)]
        [QEDLog] $log,            
        [Parameter(Mandatory=$true)]
        [string] $message,
        [string[]] $tags = @()            
    )

    $log.Write(
        [LogLevel]::Error,
        $($MyInvocation.ScriptName), 
        $($MyInvocation.ScriptLineNumber), 
        $tags,
        $message)        
}

function Write-LogInfo {
    Param(
        [Parameter(Mandatory=$true)]
        [QEDLog] $log,            
        [Parameter(Mandatory=$true)]
        [string] $message,
        [string[]] $tags = @()            
    )

    $log.Write(
        [LogLevel]::Info,
        $($MyInvocation.ScriptName), 
        $($MyInvocation.ScriptLineNumber), 
        $tags,
        $message)        
}

function Write-LogWarn {
    Param(
        [Parameter(Mandatory=$true)]
        [QEDLog] $log,            
        [Parameter(Mandatory=$true)]
        [string] $message,
        [string[]] $tags = @()            
    )

    $log.Write(
        [LogLevel]::Warn,
        $($MyInvocation.ScriptName), 
        $($MyInvocation.ScriptLineNumber), 
        $tags,
        $message)        
}

QEDLoggingTest.ps1

function Test-LogEntry {
    param
    (
        [Parameter(Mandatory=$true)]
        [LogLevel] $logLevel,
        [string[]] $tags,
        [Parameter(Mandatory=$true)]
        [string] $message
    )
    
    [QEDLogEntry] $logEntry = $null
    [DateTime] $startDate = [QEDLogEntry]::GetUtcNow()

    if ($null -eq $tags) {
        $logEntry = [QEDLogEntry]::new(
            $logLevel,
            $MyInvocation.ScriptName, 
            $MyInvocation.ScriptLineNumber,             
            $message)
        $tags = [string[]]@()            
    }

    else {
        $logEntry = [QEDLogEntry]::new(
            $logLevel,
            $MyInvocation.ScriptName, 
            $MyInvocation.ScriptLineNumber,
            $tags,         
            $message)
    }

    [DateTime] $endDate = [QEDLogEntry]::GetUtcNow()
    [string] $line = $logEntry.ToString()
    [QEDLogEntry] $logEntryFromParse = 
                        [QEDLogEntry]::new($line)
    [string] $lineFromParse = $logEntryFromParse.ToString()

    IsTrue $($startDate -lt $logEntry.GetDateTime())
    IsTrue $($endDate -gt $logEntry.GetDateTime())
    AreEqual $logLevel $logEntry.GetLogLevel()
    AreEqualCollection $tags $logEntry.GetTags()
    AreEqual $message $logEntry.GetMessage()      
    if ($lineFromParse -eq $line) {
        return $logEntry
    }

    throw "Log entries differ '$lineFromParse' -ne '$line'"

function Write-LogEntry {
    param
    (
        [Parameter(Mandatory=$true)]
        [QEDLogEntry] $logEntry
    )

    if ($logEntry.GetLogLevel() -gt $globalLogLevel) {
        return 
    }

    Write-Host $logEntry
}

[LogLevel] $globalLogLevel = [LogLevel]::Warn

function Test-LogEntries {
    [string[]] $tags = {'a', 'b', 'c'}
    [QEDLogEntry] $logEntry = Test-LogEntry `
            ([LogLevel]::Error) $tags 'A message to log'

    Write-LogEntry $logEntry
    $logEntry = Test-LogEntry `
                ([LogLevel]::Error) $null 'A message to log'            
    Write-LogEntry $logEntry
    $logEntry = Test-LogEntry `
                  ([LogLevel]::Warn) $tags 'Any log message'
    Write-LogEntry $logEntry
    $logEntry = Test-LogEntry `
                  ([LogLevel]::Warn) $null 'Any log message'
    Write-LogEntry $logEntry
    $logEntry = Test-LogEntry `
                  ([LogLevel]::Info) $tags 'Another log message'
    Write-LogEntry $logEntry
    $logEntry = Test-LogEntry `
                  ([LogLevel]::Info) $null 'Another log message'
    Write-LogEntry $logEntry
}

function Test-Log {
    Write-LogError
    Write-LogErrorRecord
    Write-LogInfo
    Write-LogWarn        
}

try {
    Test-LogEntries
    Test-Log    
    Write-Host 'Success'
}

catch {
    $errorRecord = $_    
    Write-Host $errorRecord.Exception
    Write-Host $errorRecord[0].InvocationInfo.PositionMessage
    Write-Host 'Error'
}

PsUnit.psm1

Set-StrictMode -Version 3.0

class PsUnit {
    hidden static [string] $filenameLineNumberSeparator

    hidden static [string] $filenameLineNumberStart

    hidden static [string] $filenameLineNumberEnd

    hidden static [string] $boolMismatchMessage
    
    static PsUnit() {
        [PsUnit]::filenameLineNumberStart = '('
        [PsUnit]::filenameLineNumberEnd = ')'
        [PsUnit]::filenameLineNumberSeparator = ':'
        [PsUnit]::boolMismatchMessage = 'Test expected value of '
    }

    hidden static [string] CreateMessage(
                                [string] $message, 
                                [string] $filename, 
                                [int] $lineNumber) {
        return "$message " +
            "$([PsUnit]::filenameLineNumberStart)$filename" + 
            "$([PsUnit]::filenameLineNumberSeparator)" +
            "$lineNumber$([PsUnit]::filenameLineNumberEnd)"
    }


    hidden static [void] IsTrue(
                            [bool] $shouldBeTrue, 
                            [string] $filename, 
                            [int] $lineNumber) {
        if ($shouldBeTrue) {
            return 
        }

        throw [PsUnit]::CreateMessage(
                    "$([PsUnit]::boolMismatchMessage) $true",
                    $filename,
                    $lineNumber)
    }

    hidden static [void] IsFalse(
                            [bool] $shouldBeFalse, 
                            [string] $filename, 
                            [int] $lineNumber) {
        if (!($shouldBeFalse)) {
            return 
        }

        throw [PsUnit]::CreateMessage(
                    "$([PsUnit]::boolMismatchMessage) $false",
                    $filename,
                    $lineNumber)
    }

    hidden static [bool] AreEqualCoreValidations(
                            [object] $leftValue, 
                            [object] $rightValue, 
                            [string] $filename, 
                            [int] $lineNumber) {    
        if (($null -eq $leftValue) -and ($null -eq $rightValue)) {
            return $true
        } 

        if ($null -eq $rightValue) {
            throw [PsUnit]::CreateMessage(
                "Test failed, assigned left value ($leftValue) " +
                    "cannot compared to right value of null",
                $filename,
                $lineNumber)
        } 
        
        if ($null -eq $leftValue) {
            throw [PsUnit]::CreateMessage(
                "Test failed, assigned right value " +
                    "($rightValue) cannot compared " +
                    "to left value of null",
                $filename,
                $lineNumber)
        } 

        if ($leftValue.GetType() -ne $rightValue.GetType()) {
            throw [PsUnit]::CreateMessage(        
                "Test failed type differ " +
                    "$($leftValue.GetType()) -ne " +
                    "$($rightValue.GetType())",
                $filename,
                $lineNumber)
        }        
        
        return $false        
    }

    hidden static [void] AreEqual(
                            [double] $leftValue, 
                            [double] $rightValue, 
                            [double] $tolerance,                             
                            [string] $filename, 
                            [int] $lineNumber) {
        if ([PsUnit]::AreEqualCoreValidations(
                        $leftValue, 
                        $rightValue, 
                        $filename, 
                        $lineNumber)) {
            return 
        }

        if ([System.Math]::Abs($leftValue - $rightValue) -le         
                $tolerance) {
            return 
        }

        throw [PsUnit]::CreateMessage(        
            "Test failed: difference in values " +
                "($([System.Math]::Abs($leftValue - $rightValue)))"+ 
                " exceeds tolerance ($tolerance)",
            $filename,
            $lineNumber)
    }

    hidden static [void] AreEqual(
                            [object] $leftValue, 
                            [object] $rightValue, 
                            [string] $filename, 
                            [int] $lineNumber) {    
        if ([PsUnit]::AreEqualCoreValidations(
            $leftValue, 
            $rightValue, 
            $filename, 
            $lineNumber)) {
            return 
        }

        if ($leftValue -eq $rightValue) {
            return
        }

        throw [PsUnit]::CreateMessage(        
            "Test failed: values not equal " +
                "$leftValue -ne $rightValue",
            $filename,
            $lineNumber)
    }        

    hidden static [void] AreEqualCollection(
                [System.Collections.ICollection] $leftValue,
                [System.Collections.ICollection] $rightValue, 
                [string] $filename, 
                [int] $lineNumber) {    

        if ([PsUnit]::AreEqualCoreValidations(
                        $leftValue, 
                        $rightValue, 
                        $filename, 
                        $lineNumber)) {
            return 
        }

        if ($leftValue.Count -ne $rightValue.Count) {
            throw "Collections are different item counts " +
                "($($leftValue.Count) -ne $($rightValue.Count))"
        }   

        for ($i = 0; $i -lt $leftValue.Count; $i++) {
            [PsUnit]::AreEqual(
                        $leftValue[$i], 
                        $rightValue[$i], 
                        $filename, 
                        $lineNumber)                
        }
    }        

}

function IsTrue  {
    Param(
        [Parameter(Mandatory=$true)]
        [bool] $shouldBeTrue     
    )
    
    [PsUnit]::IsTrue(
            $shouldBeTrue, 
            $MyInvocation.ScriptName, 
            $MyInvocation.ScriptLineNumber)
}

function IsFalse  {
    Param(
        [Parameter(Mandatory=$true)]
        [bool] $shouldBeFalse     
    )
     
    [PsUnit]::IsFalse(
            $shouldBeFalse, 
            $MyInvocation.ScriptName, 
            $MyInvocation.ScriptLineNumber)
}

function AreEqual  {
    Param(
        [object] $leftValue,
        [object] $rightValue             
    )

    [PsUnit]::AreEqual(
        $leftValue, 
        $rightValue, 
        $MyInvocation.ScriptName, 
        $MyInvocation.ScriptLineNumber)
}

function AreEqualCollection  {
    Param(
        [System.Collections.ICollection] $leftValue,
        [System.Collections.ICollection] $rightValue             
    )

    [PsUnit]::AreEqualCollection(
        $leftValue, 
        $rightValue, 
        $MyInvocation.ScriptName, 
        $MyInvocation.ScriptLineNumber)
}

function AreEqualDouble  {
    Param(
        [double] $leftValue,
        [double] $rightValue,
        [double] $tolerance                     
    )

    [PsUnit]::AreEqual(
        $leftValue, 
        $rightValue, 
        $tolerance,
        $MyInvocation.ScriptName, 
        $MyInvocation.ScriptLineNumber)
}

Export-ModuleMember -Function `
    IsFalse, `
    IsTrue, `
    AreEqual, `
    AreEqualCollection, `
    AreEqualDouble

PsUnitTest.ps1

Import-Module '.\PsUnit.psm1'

function Test-AreEqualUnequal {
    Param(
        [object] $leftValue,   
        [object] $rightValue                                  
    )    

    try {
        AreEqual $leftValue $rightValue
    }
    
    catch {
        return 
    }        

    throw "Exception should have been thrown " + 
        "($($MyInvocation.ScriptName):" + 
        "$($MyInvocation.ScriptLineNumber))"
}

function Test-AreEqual {
    try {
        AreEqual $false $false
        AreEqual $true $true
        AreEqual $null $null
        AreEqual 10 10
        AreEqual 'xyZ' 'XYZ'
    }
    
    catch {
        throw
    }    

    Test-AreEqualUnequal 'xyZ' $null
    Test-AreEqualUnequal $null 'xyZ'
    Test-AreEqualUnequal 'qed' $null
    Test-AreEqualUnequal $true $false
    Test-AreEqualUnequal $false $true
    Test-AreEqualUnequal 10 5
    Test-AreEqualUnequal 'x' 5
}

function Test-AreEqualUnequalCollection {
    Param(
        [System.Collections.ICollection] $leftValue,   
        [System.Collections.ICollection] $rightValue                                  
    )    

    try {
        AreEqualCollection $leftValue $rightValue
    }
    
    catch {
        return 
    }        

    throw "Exception should have been thrown " +
        "($($MyInvocation.ScriptName):" + 
        "$($MyInvocation.ScriptLineNumber))"
}

function Test-AreEqualCollection {
    [string[]] $leftStrings = @()
    [string[]] $rightStrings = @()
    [int[]] $leftInts = @()
    [int[]] $rightInts = @()
    [bool[]] $leftBools = @()
    [bool[]] $rightBools = @()

    try {
        AreEqualCollection $null $null
        AreEqualCollection $leftStrings $rightStrings
        $leftStrings = @('x', 'y', 'z')
        $rightStrings = @('x', 'y', 'z')
        AreEqualCollection $leftStrings $rightStrings
        AreEqualCollection $leftInts $rightInts
        $leftInts = @(1, 2, 3, 4)
        $rightInts = @(1, 2, 3, 4)
        AreEqualCollection $leftInts $rightInts
        AreEqualCollection $leftBools $rightBools
        $leftBools = @($true, $false, $true, $true)
        $rightBools = @($true, $false, $true, $true)
        AreEqualCollection $leftBools $rightBools
    }
    
    catch {
        throw
    }    

    Test-AreEqualUnequalCollection $null $rightStrings
    Test-AreEqualUnequalCollection $null $rightInts
    Test-AreEqualUnequalCollection $null $rightBools
    Test-AreEqualUnequalCollection $leftStrings $null
    Test-AreEqualUnequalCollection $leftInts $null
    Test-AreEqualUnequalCollection $leftBools $null
    Test-AreEqualUnequalCollection $leftStrings ([string[]]@())
    Test-AreEqualUnequalCollection $leftInts ([int[]]@())
    Test-AreEqualUnequalCollection $leftBools ([bool[]]@())
    Test-AreEqualUnequalCollection ([string[]]@()) $rightStrings
    Test-AreEqualUnequalCollection ([int[]]@()) $rightInts
    Test-AreEqualUnequalCollection ([bool[]]@()) $rightBools
    $leftStrings = @('x', 'y')
    $leftInts = @(1, 2, 3)
    $leftBools = @($true, $false, $true)
    Test-AreEqualUnequalCollection $leftStrings $rightStrings
    Test-AreEqualUnequalCollection $leftInts $rightInts
    Test-AreEqualUnequalCollection $leftBools $rightBools
    Test-AreEqualUnequalCollection $leftStrings $rightInts
    Test-AreEqualUnequalCollection $leftStrings $rightBools
    Test-AreEqualUnequalCollection $leftInts $rightStrings
    Test-AreEqualUnequalCollection $leftInts $rightBools
}

function Test-AreEqualUnequalDouble {
    Param(
        [double] $leftValue,   
        [double] $rightValue,
        [double] $tolerance                                          
    )    

    try {
        AreEqualDouble $leftValue $rightValue $tolerance 
    }
    
    catch {
        return 
    }        

    throw "Exception should have been thrown " +
        "($($MyInvocation.ScriptName):" +
        "$($MyInvocation.ScriptLineNumber))"
}

function Test-AreEqualDouble {
    [double] $value01 = 0.1
    [double] $value02 = 0.11
    [double] $tolerance = 0.015

    try {
        AreEqualDouble $value01 $value02 $tolerance
        $value02 = 0.09
        AreEqualDouble $value01 $value02 $tolerance
        $value01 = 100.0
        $value02 = 110.0
        $tolerance = 10.001
        AreEqualDouble $value01 $value02 $tolerance
        $value01 = -100.0
        $value02 = -110.0
        $tolerance = 10.001
        AreEqualDouble $value01 $value02 $tolerance
        $value01 = 0.01
        $value02 = 0.01
        $tolerance = 0
        AreEqualDouble $value01 $value02 $tolerance    
    }
    
    catch {
        throw
    }    

    $value01 = 100.0
    $value02 = 110.0
    $tolerance = 9.999
    Test-AreEqualUnequal $value01 $value02 $tolerance
    $value01 = -100.0
    $value02 = -110.0
    Test-AreEqualUnequal $value01 $value02 $tolerance
    $value01 = 0.01
    $value02 = 0.0101
    $tolerance = 00009
    Test-AreEqualUnequal $value01 $value02 $tolerance
    Test-AreEqualUnequal $value01 $value02 $tolerance
    $value01 = -0.01
    $value02 = -0.0101
    Test-AreEqualUnequal $value01 $value02 $tolerance
}

function Test-IsFalse {
    [bool] $expectedException = $false

    try {
        IsFalse $false
        IsFalse $($null -eq '')
        $expectedException = $true
        IsFalse $true
    }
    
    catch {
        if ($expectedException) {
            return
        }

        throw
    }    
}

function Test-IsTrue {
    [bool] $expectedException = $false

    try {
        IsTrue $true
        IsTrue $('AbC' -eq 'aBC')
        $expectedException = $true
        IsTrue $false
    }
    
    catch {
        if ($expectedException) {
            return
        }

        throw
    }    
}

try {
    Test-AreEqual
    Test-AreEqualCollection
    Test-AreEqualDouble    
    Test-IsFalse
    Test-IsTrue
    Write-Host 'Success'

}

catch {
    $errorRecord = $_    
    Write-Host "Error: $errorRecord.Exception"
    Write-Host 'Failure'
}

No comments :

Post a Comment