src/formatters.ps1


using namespace System.Collections.Generic

[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
Param()

$ErrorActionPreference = "Stop"
."$PSScriptRoot\types.ps1"

function writePending($timestamp, $description) {
    $symbol = " "
    $color = "Yellow"
    $message = "$timestamp [ $symbol ] $description"
    Write-Host $message -ForegroundColor $color -NoNewline
}

function writeSuccess($timestamp, $description, $clearString) {
    $symbol = [char]8730
    $color = "Green"
    $message = "$timestamp [ $symbol ] $description"
    Write-Host "`r$clearString" -NoNewline
    Write-Host "`r$message" -ForegroundColor $color
}

function writeFail($timestamp, $description, $clearString) {
    $symbol = "X"
    $color = "Red"
    $message = "$timestamp [ $symbol ] $description"
    Write-Host "`r$clearString" -NoNewline
    Write-Host "`n$message`n" -ForegroundColor $color
    exit -1
}

$fsm = @{
    "Test Test Start $false"    = {
        writePending @args
        @{
            "Test Test Stop $true"  = {
                writeSuccess @args
                $fsm
            }
            "Test Test Stop $false" = {
                writeFail @args
            }
        }
    }
    "Set Set Start $false"      = {
        writePending @args
        @{
            "Set Set Stop $false" = {
                writeSuccess @args
                $fsm
            }
        }
    }
    "TestSet Test Start $false" = {
        writePending @args
        @{
            "TestSet Test Stop $true"  = {
                writeSuccess @args
                $fsm
            }
            "TestSet Test Stop $false" = {
                @{
                    "TestSet Set Start $false" = {
                        @{
                            "TestSet Set Stop $false" = {
                                @{
                                    "TestSet Validate Start $false" = {
                                        @{
                                            "TestSet Validate Stop $true"  = {
                                                writeSuccess @args
                                                $fsm
                                            }
                                            "TestSet Validate Stop $false" = {
                                                writeFail @args
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

<#
.SYNOPSIS
  Formats Requirement log events as a live-updating checklist
.NOTES
  Uses Write-Host
#>

function Format-Checklist {
    [CmdletBinding()]
    Param(
        # Logged Requirement lifecycle events
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias("Event")]
        [RequirementEvent[]]$RequirementEvent
    )

    begin {
        $previousRequirement = $null
        $nextFsm = $fsm
    }

    process {
        $requirement = $_.Requirement

        # build state vector
        $requirementType = ("Test", "Set" | ? { $requirement.$_ }) -join ""
        $method = $_.Method
        $lifecycleState = $_.State
        $successResult = [bool]$_.Result
        $stateVector = "$requirementType $method $lifecycleState $successResult"

        # build transition arguments
        $timestamp = Get-Date -Date $_.Date -Format "hh:mm:ss"
        $description = $requirement.Describe
        $clearString = ' ' * "??:??:?? [ ? ] $($previousRequirement.Describe)".Length
        $transitionArgs = @($timestamp, $description, $clearString)

        # transition FSM
        if (-not $nextFsm[$stateVector]) {
            throw @"
Format-Checklist has reached an unexpected state '$stateVector'.
If you are piping the output of Invoke-Requirement directly to this
cmdlet, then this is probably a bug in Format-Checklist.
"@

        }
        $nextFsm = &$nextFsm[$stateVector] @transitionArgs
        $previousRequirement = $requirement
    }
}

<#
.SYNOPSIS
  Formats every log event with metadata, including a stack of requirement names when using nested Requirements
.NOTES
  Uses Write-Host
#>

function Format-CallStack {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    [CmdletBinding()]
    Param(
        # Logged Requirement lifecycle events
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias("Event")]
        [RequirementEvent[]]$RequirementEvent
    )

    begin {
        $context = [Stack[string]]::new()
    }

    process {
        $timestamp = Get-Date -Date $_.Date -Format 'hh:mm:ss'
        $name = $_.Requirement.Name
        $description = $_.Requirement.Describe
        $method, $state, $result = $_.Method, $_.State, $_.Result
        switch ($method) {
            "Test" {
                switch ($state) {
                    "Start" {
                        $context.Push($name)
                        $callstack = $context.ToArray()
                        [array]::Reverse($callstack)
                        $serialized = $callstack -join ">"
                        Write-Host "$timestamp [$serialized] BEGIN TEST $description"
                    }
                    "Stop" {
                        $callstack = $context.ToArray()
                        [array]::Reverse($callstack)
                        $serialized = $callstack -join ">"
                        Write-Host "$timestamp [$serialized] END TEST => $result"
                        $context.Pop() | Out-Null
                    }
                }
            }
            "Set" {
                switch ($state) {
                    "Start" {
                        $context.Push($name)
                        $callstack = $context.ToArray()
                        [array]::Reverse($callstack)
                        $serialized = $callstack -join ">"
                        Write-Host "$timestamp [$serialized] BEGIN SET $description"
                    }
                    "Stop" {
                        $callstack = $context.ToArray()
                        [array]::Reverse($callstack)
                        $serialized = $callstack -join ">"
                        Write-Host "$timestamp [$serialized] END SET"
                        $context.Pop() | Out-Null
                    }
                }
            }
            "Validate" {
                switch ($state) {
                    "Start" {
                        $context.Push($name)
                        $callstack = $context.ToArray()
                        [array]::Reverse($callstack)
                        $serialized = $callstack -join ">"
                        Write-Host "$timestamp [$serialized] BEGIN TEST $description"
                    }
                    "Stop" {
                        $callstack = $context.ToArray()
                        [array]::Reverse($callstack)
                        $serialized = $callstack -join ">"
                        Write-Host "$timestamp [$serialized] END TEST => $result"
                        $context.Pop() | Out-Null
                    }
                }
            }
        }
    }
}