From d7a5d6eb490defc8d9ff285a83e87858658962cb Mon Sep 17 00:00:00 2001 From: Chris Kuech Date: Thu, 5 Sep 2019 21:01:33 -0400 Subject: [PATCH] Format-Checklist rewrite (#28) * reimplemented Format-Checklist to cover more scenarios * bumped version number --- .vscode/settings.json | 2 +- PSScriptAnalyzerSettings.psd1 | 24 +++---- Requirements.psd1 | 164 +++++++++++++++++++++--------------------- example.ps1 | 12 +++- src/formatters.ps1 | 162 +++++++++++++++++++++++++++-------------- 5 files changed, 214 insertions(+), 150 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d741c03..7a3c4be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1" + "powershell.scriptAnalysis.settingsPath": "./PSScriptAnalyzerSettings.psd1" } \ No newline at end of file diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 index 9b2ef36..65cb4b9 100644 --- a/PSScriptAnalyzerSettings.psd1 +++ b/PSScriptAnalyzerSettings.psd1 @@ -1,15 +1,15 @@ @{ - 'Rules' = @{ - 'PSAvoidUsingCmdletAliases' = @{ - 'Whitelist' = @( - '?', - '%', - 'foreach', - 'group', - 'measure', - 'select', - 'where' - ) + 'Rules' = @{ + 'PSAvoidUsingCmdletAliases' = @{ + 'Whitelist' = @( + '?', + '%', + 'foreach', + 'group', + 'measure', + 'select', + 'where' + ) + } } - } } \ No newline at end of file diff --git a/Requirements.psd1 b/Requirements.psd1 index 95ff9c4..53857d6 100644 --- a/Requirements.psd1 +++ b/Requirements.psd1 @@ -1,124 +1,124 @@ @{ - # Script module or binary module file associated with this manifest. - RootModule = 'src\module.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'src\module.psm1' - # Version number of this module. - ModuleVersion = '2.2.3' + # Version number of this module. + ModuleVersion = '2.2.4' - # Supported PSEditions - # CompatiblePSEditions = @() + # Supported PSEditions + # CompatiblePSEditions = @() - # ID used to uniquely identify this module - GUID = 'c14a3682-ef9f-40ed-9521-ef74b433a755' + # ID used to uniquely identify this module + GUID = 'c14a3682-ef9f-40ed-9521-ef74b433a755' - # Author of this module - Author = 'Chris Kuech' + # Author of this module + Author = 'Chris Kuech' - # Company or vendor of this module - CompanyName = 'Microsoft Corporation' + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' - # Copyright statement for this module - Copyright = '(c) 2018 Microsoft Corporation. All rights reserved.' + # Copyright statement for this module + Copyright = '(c) 2018 Microsoft Corporation. All rights reserved.' - # Description of the functionality provided by this module - Description = 'PowerShell framework for declaratively defining and idempotently imposing system configurations' + # Description of the functionality provided by this module + Description = 'PowerShell framework for declaratively defining and idempotently imposing system configurations' - # Minimum version of the Windows PowerShell engine required by this module - # PowerShellVersion = '' + # Minimum version of the Windows PowerShell engine required by this module + # PowerShellVersion = '' - # Name of the Windows PowerShell host required by this module - # PowerShellHostName = '' + # Name of the Windows PowerShell host required by this module + # PowerShellHostName = '' - # Minimum version of the Windows PowerShell host required by this module - # PowerShellHostVersion = '' + # Minimum version of the Windows PowerShell host required by this module + # PowerShellHostVersion = '' - # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. - # DotNetFrameworkVersion = '' + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' - # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. - # CLRVersion = '' + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # CLRVersion = '' - # Processor architecture (None, X86, Amd64) required by this module - # ProcessorArchitecture = '' + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' - # Modules that must be imported into the global environment prior to importing this module - # RequiredModules = @() + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() - # Assemblies that must be loaded prior to importing this module - # RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() - # Script files (.ps1) that are run in the caller's environment prior to importing this module. - ScriptsToProcess = @( - 'src\types.ps1' - ) + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + ScriptsToProcess = @( + 'src\types.ps1' + ) - # Type files (.ps1xml) to be loaded when importing this module - # TypesToProcess = @() + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() - # Format files (.ps1xml) to be loaded when importing this module - # FormatsToProcess = @() + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() - # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess - # NestedModules = @() + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() - # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @( - 'Format-CallStack', - 'Format-Checklist', - 'Invoke-Requirement', - 'New-Requirement', - 'Set-Requirement', - 'Test-Requirement' - ) + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Format-CallStack', + 'Format-Checklist', + 'Invoke-Requirement', + 'New-Requirement', + 'Set-Requirement', + 'Test-Requirement' + ) - # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @() + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() - # Variables to export from this module - VariablesToExport = '*' + # Variables to export from this module + VariablesToExport = '*' - # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @() + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() - # DSC resources to export from this module - # DscResourcesToExport = @() + # DSC resources to export from this module + # DscResourcesToExport = @() - # List of all modules packaged with this module - # ModuleList = @() + # List of all modules packaged with this module + # ModuleList = @() - # List of all files packaged with this module - # FileList = @() + # List of all files packaged with this module + # FileList = @() - # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ - PSData = @{ + PSData = @{ - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() - # A URL to the license for this module. - LicenseUri = 'https://github.com/Microsoft/Requirements/blob/master/LICENSE' + # A URL to the license for this module. + LicenseUri = 'https://github.com/Microsoft/Requirements/blob/master/LICENSE' - # A URL to the main website for this project. - ProjectUri = 'https://github.com/Microsoft/Requirements' + # A URL to the main website for this project. + ProjectUri = 'https://github.com/Microsoft/Requirements' - # A URL to an icon representing this module. - # IconUri = '' + # A URL to an icon representing this module. + # IconUri = '' - # ReleaseNotes of this module - # ReleaseNotes = '' + # ReleaseNotes of this module + # ReleaseNotes = '' - } # End of PSData hashtable + } # End of PSData hashtable - } # End of PrivateData hashtable + } # End of PrivateData hashtable - # HelpInfo URI of this module - # HelpInfoURI = '' + # HelpInfo URI of this module + # HelpInfoURI = '' - # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. - # DefaultCommandPrefix = '' + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' } diff --git a/example.ps1 b/example.ps1 index fadf29c..86e941b 100644 --- a/example.ps1 +++ b/example.ps1 @@ -2,8 +2,6 @@ $ErrorActionPreference = "Stop" Import-Module "$PSScriptRoot\Requirements.psd1" -Force - - $requirements = @( @{ Name = "Resource 1" @@ -17,6 +15,11 @@ $requirements = @( Test = { $mySystem -contains 2 } Set = { $mySystem.Add(2) | Out-Null; Start-Sleep 1 } }, + @{ + Name = "Checkpoint" + Describe = "Assert before continuing" + Test = { $mySystem } + }, @{ Name = "Resource 3" Describe = "Resource 3 is present in the system" @@ -29,6 +32,11 @@ $requirements = @( Test = { $mySystem -contains 4 } Set = { throw "This should not have been reached!"; Start-Sleep 1 } }, + @{ + Name = "Always set" + Describe = "Always run the set command" + Set = { Start-Sleep 1 } + }, @{ Name = "Resource 5" Describe = "Resource 5 is present in the system" diff --git a/src/formatters.ps1 b/src/formatters.ps1 index ca36c0b..a50ed9e 100644 --- a/src/formatters.ps1 +++ b/src/formatters.ps1 @@ -1,9 +1,92 @@ 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 @@ -11,7 +94,6 @@ $ErrorActionPreference = "Stop" Uses Write-Host #> function Format-Checklist { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] Param( # Logged Requirement lifecycle events @@ -21,62 +103,36 @@ function Format-Checklist { ) begin { - $lastDescription = "" + $previousRequirement = $null + $nextFsm = $fsm } process { - $timestamp = Get-Date -Date $_.Date -Format 'hh:mm:ss' - $description = $_.Requirement.Describe - $method, $state, $result = $_.Method, $_.State, $_.Result - switch ($method) { - "Test" { - switch ($state) { - "Start" { - $symbol = " " - $color = "Yellow" - $message = "$timestamp [ $symbol ] $description" - Write-Host $message -ForegroundColor $color -NoNewline - $lastDescription = $description - } - "Stop" { - switch ($result) { - $true { - $symbol = [char]8730 - $color = "Green" - $message = "$timestamp [ $symbol ] $description" - Write-Host "`r$(' ' * $lastDescription.Length)" -NoNewline - Write-Host "`r$message" -ForegroundColor $color - $lastDescription = $description - } - } - } - } - } - "Validate" { - switch ($state) { - "Stop" { - switch ($result) { - $true { - $symbol = [char]8730 - $color = "Green" - $message = "$timestamp [ $symbol ] $description" - Write-Host "`r$(' ' * $lastDescription.Length)" -NoNewline - Write-Host "`r$message" -ForegroundColor $color - $lastDescription = $description - } - $false { - $symbol = "X" - $color = "Red" - $message = "$timestamp [ $symbol ] $description" - Write-Host "`n$message`n" -ForegroundColor $color - $lastDescription = $description - exit -1 - } - } - } - } - } + $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 } }