Better (#2)
New major version. Interface and implementation have been completely overhauled. The previous version included Invoke-ChecklistRequirement and Invoke-ContextRequirement, which relied on global state and held two different requirement engines with integrated logging. The new version contains a single requirement engine that outputs RequirementEvents. These events can be displayed with standard PowerShell formatters like Format-Table and Format-List. In addition, this PR introduces Format-Checklist for the human-facing interface and Format-Callstack for verbosely logging requirement execution In addition to cleaner interfaces and implementation, Requirements now supports "DependsOn" for dependency graph execution.chriskuech-patch-1
parent
c0d1273ef3
commit
8dcfa31b17
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1"
|
||||
}
|
181
Checklist.psm1
181
Checklist.psm1
|
@ -1,181 +0,0 @@
|
|||
|
||||
enum Status {
|
||||
NotRun
|
||||
Pass
|
||||
Fail
|
||||
}
|
||||
|
||||
|
||||
|
||||
class Log {
|
||||
|
||||
# We originally used [Status] values as keys instead of casting [Status] values to strings.
|
||||
# We cast values to strings for now due to a bug in PSScriptAnalyzer v1.16.1
|
||||
static [hashtable] $Symbols = @{
|
||||
Pass = [char]8730
|
||||
Fail = "X"
|
||||
NotRun = " "
|
||||
}
|
||||
static [hashtable] $Colors = @{
|
||||
Pass = "Green"
|
||||
NotRun = "Yellow"
|
||||
Fail = "Red"
|
||||
}
|
||||
static [int] $LastLineLength
|
||||
|
||||
static [void] WriteLine([string] $message, [Status] $status) {
|
||||
$message = [Log]::FormatMessage($message, $status)
|
||||
[Log]::LastLineLength = $message.Length
|
||||
$color = [Log]::Colors[ [string]$status ]
|
||||
Write-Host $message -ForegroundColor $color -NoNewline
|
||||
}
|
||||
|
||||
static [void] OverwriteLine([string] $message, [Status] $status) {
|
||||
Write-Host "`r$(' ' * [Log]::LastLineLength)" -NoNewline
|
||||
$message = [Log]::FormatMessage($message, $status)
|
||||
[Log]::LastLineLength = $message.Length
|
||||
$color = [Log]::Colors[ [string]$status ]
|
||||
Write-Host "`r$message" -ForegroundColor $color
|
||||
}
|
||||
|
||||
static [string] FormatMessage([string] $message, [Status] $status) {
|
||||
$symbol = [Log]::Symbols[ [string]$status ]
|
||||
return "$(Get-Date -Format 'hh:mm:ss') [ $symbol ] $message"
|
||||
}
|
||||
|
||||
static [void] WriteError([string] $message) {
|
||||
Write-Host "`n$message`n" -ForegroundColor Red
|
||||
exit -1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Ensures a requirement is met.
|
||||
|
||||
.DESCRIPTION
|
||||
This cmdlet allows for declaratively defining requirements and implementing consistent logging and idempotency around the status of the requirements.
|
||||
|
||||
.PARAMETER Describe
|
||||
A description of the requirement that is enforced.
|
||||
|
||||
.PARAMETER Test
|
||||
If present, 'Test' is a scriptblock that returns 'true' if the requirement is already met and the 'Set' scriptblock should not run. If not present, 'Set' will always run.
|
||||
|
||||
.PARAMETER Set
|
||||
A scriptblock that imposes the requirement when run. If a "Test' scriptblock is not provided, 'Set' must be idempotent.
|
||||
|
||||
.PARAMETER Message
|
||||
An error message printed if an idempotent 'Set' scriptblock fails during execution.
|
||||
|
||||
.EXAMPLE
|
||||
# A non-idempotent 'Set' scriptblock
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe "'Hello world' is logged" `
|
||||
-Test {Get-Content $MyLogFilePath | ? {$_ -eq "Hello world"}} `
|
||||
-Set {"Hello world" >> $MyLogFilePath}
|
||||
|
||||
# An idempotent 'Set' scriptblock
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe "'Hello world' is logged" `
|
||||
-Set {"Hello world" > $MyLogFilePath} `
|
||||
-Message "Could not log 'Hello World'"
|
||||
#>
|
||||
function Invoke-ChecklistRequirement {
|
||||
Param(
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
|
||||
[Parameter(Mandatory, ParameterSetName = "Information")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Describe,
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[scriptblock] $Test,
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[scriptblock] $Set,
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Message,
|
||||
[switch] $ListRequirement
|
||||
)
|
||||
|
||||
|
||||
try {
|
||||
|
||||
if ($ListRequirement) {
|
||||
[Log]::WriteLine("$Describe`n", [Status]::NotRun)
|
||||
return
|
||||
}
|
||||
|
||||
switch ($PSCmdlet.ParameterSetName) {
|
||||
|
||||
"ApplyIfNeeded" {
|
||||
[Log]::WriteLine($Describe, [Status]::NotRun)
|
||||
if (&$Test) {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Pass)
|
||||
}
|
||||
else {
|
||||
&$Set | Out-Null
|
||||
if (&$Test) {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Pass)
|
||||
}
|
||||
else {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Fail)
|
||||
[Log]::WriteError("Requirement validation failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"ApplyAlways" {
|
||||
[Log]::WriteLine($Describe, [Status]::NotRun)
|
||||
if (&$Set) {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Pass)
|
||||
}
|
||||
else {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Fail)
|
||||
[Log]::WriteError($Message)
|
||||
}
|
||||
}
|
||||
|
||||
"Information" {
|
||||
[Log]::WriteLine("$Describe`n", [Status]::NotRun)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Fail)
|
||||
Write-Host ""
|
||||
throw $_
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Invoke-ChecklistDscRequirement {
|
||||
Param(
|
||||
[string]$Describe,
|
||||
[string]$ResourceName,
|
||||
[string]$ModuleName,
|
||||
[hashtable]$Property
|
||||
)
|
||||
|
||||
$dscParams = @{
|
||||
Name = $ResourceName
|
||||
ModuleName = $ModuleName
|
||||
Property = $Property
|
||||
}
|
||||
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe $Describe `
|
||||
-Test {Invoke-DscResource -Method "Test" @dscParams} `
|
||||
-Set {Invoke-DscResource -Method "Set" @dscParams}
|
||||
|
||||
}
|
98
Context.psm1
98
Context.psm1
|
@ -1,98 +0,0 @@
|
|||
# TODO: (MEDIUM) Implement cmdlets wrapping the class implementation
|
||||
# TODO: (MEDIUM) Break out Configuration\ConfigurationManager into Modules\ConfigurationManager
|
||||
# TODO: (MEDIUM) Ensure stack traces propogate from module functions
|
||||
|
||||
using namespace System.Collections.Generic
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$InformationPreference = "Continue"
|
||||
|
||||
$LogContext = [Stack[string]]::new()
|
||||
|
||||
|
||||
|
||||
$DockerLinePrefix = " ----->"
|
||||
|
||||
|
||||
function Write-Log {
|
||||
Param(
|
||||
[Parameter(Mandatory, Position=0, ParameterSetName="PushContext")]
|
||||
[Parameter(Mandatory, Position=0, ParameterSetName="ExistingContext")]
|
||||
[string] $Context,
|
||||
[Parameter(Mandatory, Position=1, ParameterSetName="PushContext")]
|
||||
[scriptblock] $ScriptBlock
|
||||
)
|
||||
|
||||
switch ($PSCmdlet.ParameterSetName) {
|
||||
"PushContext" {
|
||||
$LogContext.Push($Context)
|
||||
Write-Log "<begin>"
|
||||
$result = &$ScriptBlock
|
||||
Write-Log "<end>"
|
||||
$LogContext.Pop() | Out-Null
|
||||
if ($result -is [object]) {
|
||||
return $result
|
||||
} else {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
"ExistingContext" {
|
||||
$stack = $LogContext.ToArray()
|
||||
[array]::Reverse($stack)
|
||||
$prefix = "$DockerLinePrefix $(Get-Date -Format "hh:mm:ss") [$($stack -join " > ")]"
|
||||
Write-Information "$prefix $Context"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Invoke-ContextRequirement {
|
||||
Param(
|
||||
[string]$Name,
|
||||
[scriptblock]$Test,
|
||||
[scriptblock]$Set
|
||||
)
|
||||
|
||||
try {
|
||||
Write-Log $Name {
|
||||
|
||||
$requirementAlreadyMet = Write-Log "Test" {&$Test}
|
||||
if (-not $requirementAlreadyMet) {
|
||||
Write-Log "Set" {&$Set | Out-Null}
|
||||
$requirementValidated = Write-Log "Test" {&$Test}
|
||||
if (-not $requirementValidated) {
|
||||
throw "Requirement validation failed"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Invoke-ContextDscRequirement {
|
||||
Param(
|
||||
[string]$Name,
|
||||
[string]$ResourceName,
|
||||
[string]$ModuleName,
|
||||
[hashtable]$Property
|
||||
)
|
||||
|
||||
$dscParams = @{
|
||||
Name = $ResourceName
|
||||
ModuleName = $ModuleName
|
||||
Property = $Property
|
||||
}
|
||||
|
||||
Invoke-Requirement `
|
||||
-Name $Name `
|
||||
-Test {Invoke-DscResource -Method "Test" @dscParams} `
|
||||
-Set {Invoke-DscResource -Method "Set" @dscParams}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
@{
|
||||
'Rules' = @{
|
||||
'PSAvoidUsingCmdletAliases' = @{
|
||||
'Whitelist' = @(
|
||||
'?',
|
||||
'%',
|
||||
'foreach',
|
||||
'group',
|
||||
'measure',
|
||||
'select',
|
||||
'where'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
113
README.md
113
README.md
|
@ -1,3 +1,116 @@
|
|||
# Requirements
|
||||
Requirements is a PowerShell Gallery module for declaratively describing a system as a set of "requirements", then idempotently setting each requirement to its desired state.
|
||||
|
||||
## Usage
|
||||
|
||||
We use the term `Test` to refer to the condition that describes whether the Requirement is in its desired state. We use the term `Set` to refer to the command that a `Requirement` uses to put itself in its desired state if it is known to not be in its desired state.
|
||||
|
||||
### Declaring requirements
|
||||
The easiest way to declare a requirement is to define it as a hashtable and let PowerShell's implicit casting handle the rest.
|
||||
|
||||
```powershell
|
||||
$requirements = @(
|
||||
@{
|
||||
Name = "Resource 1"
|
||||
Describe = "Resource 1 is present in the system"
|
||||
Test = { $mySystem -contains 1 }
|
||||
Set = { $mySystem.Add(1) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 2"
|
||||
Describe = "Resource 2 is present in the system"
|
||||
Test = { $mySystem -contains 2 }
|
||||
Set = { $mySystem.Add(2) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 3"
|
||||
Describe = "Resource 3 is present in the system"
|
||||
Test = { $mySystem -contains 3 }
|
||||
Set = { $mySystem.Add(3) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 4"
|
||||
Describe = "Resource 4 is present in the system"
|
||||
Test = { $mySystem -contains 4 }
|
||||
Set = { $mySystem.Add(4) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 5"
|
||||
Describe = "Resource 5 is present in the system"
|
||||
Test = { $mySystem -contains 5 }
|
||||
Set = { $mySystem.Add(5) | Out-Null; Start-Sleep 1 }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Idempotently `Set`ting requirements
|
||||
Simply pipe an array of `Requirement`s to `Invoke-Requirement`
|
||||
|
||||
```powershell
|
||||
$requirements | Invoke-Requirement
|
||||
```
|
||||
|
||||
### Formatting the logs
|
||||
`Invoke-Requirement` will output logging events for each step in a `Requirement`'s execution lifecycle. You can capture these logs with `Format-Table` or `Format-List`, or
|
||||
|
||||
```powershell
|
||||
$requirements | Invoke-Requirement | Format-Table
|
||||
```
|
||||
|
||||
#### `Format-Table`
|
||||
These logs were using `-Autosize` parameter, which better formats the columns, but does not support outputting as a stream.
|
||||
```
|
||||
Method Lifecycle Name Date
|
||||
------ --------- ---- ----
|
||||
Test Start Resource 1 6/12/2019 12:00:25 PM
|
||||
Test Stop Resource 1 6/12/2019 12:00:25 PM
|
||||
Set Start Resource 1 6/12/2019 12:00:25 PM
|
||||
Set Stop Resource 1 6/12/2019 12:00:26 PM
|
||||
Validate Start Resource 1 6/12/2019 12:00:26 PM
|
||||
Validate Stop Resource 1 6/12/2019 12:00:26 PM
|
||||
Test Start Resource 2 6/12/2019 12:00:26 PM
|
||||
Test Stop Resource 2 6/12/2019 12:00:26 PM
|
||||
Set Start Resource 2 6/12/2019 12:00:26 PM
|
||||
...
|
||||
```
|
||||
|
||||
#### `Format-Checklist`
|
||||
`Format-Checklist` will present a live-updating checklist to the user.
|
||||
|
||||
![Format-Checklist output](https://raw.githubusercontent.com/microsoft/requirements/master/imgs/checklist.png)
|
||||
|
||||
#### `Format-Callstack`
|
||||
Unlike `Format-Checklist`, `Format-Callstack` prints all log events and includes metadata. For complex use cases, you can define nested `Requirement`s (`Requirement`s that contain more `Requirement`s in their `Set` block). `Format-Callstack` will print the stack of `Requirement` names of each `Requirement` as its processed.
|
||||
|
||||
![Format-Callstack output](https://raw.githubusercontent.com/microsoft/requirements/master/imgs/callstack.png)
|
||||
|
||||
### Defining DSC Resources
|
||||
If you're using Windows and PowerShell 5, you can use DSC resources with Requirements.
|
||||
|
||||
```PowerShell
|
||||
$requirement = @{
|
||||
Describe = "My Dsc Requirement"
|
||||
ResourceName = "File"
|
||||
ModuleName = "PSDesiredStateConfiguration"
|
||||
Property = @{
|
||||
Contents = "Hello World"
|
||||
DestinationFile = "C:\myFile.txt"
|
||||
Force = $true
|
||||
}
|
||||
}
|
||||
New-Requirement @requirement | Invoke-Requirement | Format-Checklist
|
||||
```
|
||||
|
||||
## Comparison to DSC
|
||||
Desired State Configurations allow you to declaratively describe a configuration then let the configuration manager handle with setting the configuration to its desired state. This pattern from the outside may seem similar to Requirements, but there are crucial differences.
|
||||
|
||||
DSC is optimized for handling *many* configurations *asynchronously*. For example, applying a configuration in parallel to multiple nodes. In contrast, Requirements applies a *single* configuration *synchronously*. This enables usage in different scenarios, including:
|
||||
* CI/CD scripts
|
||||
* CLIs
|
||||
* Dockerfiles
|
||||
* Linux
|
||||
|
||||
While Requirements supports DSC resources, it does not have a hard dependency on DSC's configuration manager, so if your Requirements do not include DSC resources they will work on any platform that PowerShell Core supports.
|
||||
|
||||
# Contributing
|
||||
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
|
||||
Import-Module Pester
|
||||
Import-Module .\Requirements
|
||||
|
||||
|
||||
Describe "Invoke-ChecklistRequirement" {
|
||||
|
||||
It "Should write to host correctly" {
|
||||
# convert Host stream to array by writing to then reading from file
|
||||
$tempFile = "$env:TEMP\$(New-Guid).out.txt"
|
||||
try {
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe "Simple Requirement" `
|
||||
-Test { $script:ValidRequirementHasRun } `
|
||||
-Set { $script:ValidRequirementHasRun = $true } `
|
||||
*> $tempFile
|
||||
$output = Get-Content $tempFile
|
||||
$loggedOutput = $output | ? {$_.Trim()}
|
||||
$clearedOutput = $output | ? {-not $_.Trim()}
|
||||
$loggedOutput | % {$_ | Should -Match "^\d\d:\d\d:\d\d \[ . \] Simple Requirement$"}
|
||||
$clearedOutput.Count | Should -Be 1
|
||||
$loggedOutput.Count | Should -Be 2
|
||||
} finally {
|
||||
Remove-Item $tempFile
|
||||
}
|
||||
}
|
||||
|
||||
It "Should not 'Set' if in desired state" {
|
||||
$script:NotSetIfInDesiredState = 0
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe "Simple Requirement" `
|
||||
-Test { $true } `
|
||||
-Set { $script:NotSetIfInDesiredState++ } `
|
||||
*> $null
|
||||
$script:NotSetIfInDesiredState | Should -Be 0
|
||||
}
|
||||
|
||||
It "Should 'Set' if in desired state" {
|
||||
$script:SetIfInDesiredState = 0
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe "Simple Requirement" `
|
||||
-Test {$script:SetIfInDesiredState -gt 0} `
|
||||
-Set {$script:SetIfInDesiredState++} `
|
||||
*> $null
|
||||
$script:SetIfInDesiredState | Should -Be 1
|
||||
}
|
||||
|
||||
It "Should validate once set" {
|
||||
$script:TestOnceSetIsTestCount = 0
|
||||
$script:TestOnceSetIsSet = $false
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe "Simple Requirement" `
|
||||
-Test {$script:TestOnceSetIsTestCount += 1; $script:TestOnceSetIsSet} `
|
||||
-Set {$script:TestOnceSetIsSet = $true} `
|
||||
*> $null
|
||||
$script:TestOnceSetIsSet | Should -Be $true
|
||||
$script:TestOnceSetIsTestCount | Should -Be 2
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
97
classes.ps1
97
classes.ps1
|
@ -1,97 +0,0 @@
|
|||
|
||||
using namespace System.Collections.Generic
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$InformationPreference = "Continue"
|
||||
#$VerbosePreference = "Continue"
|
||||
|
||||
Import-Module "Profile"
|
||||
|
||||
|
||||
|
||||
class Resource {
|
||||
[bool] Test() {
|
||||
throw "Abstract method"
|
||||
return $false
|
||||
}
|
||||
[void] Set() {
|
||||
throw "Abstract method"
|
||||
}
|
||||
[string] ToString() {
|
||||
throw "Abstract method"
|
||||
return ""
|
||||
}
|
||||
static [void] Run([resource[]]$resources) {
|
||||
foreach ($resource in $resources) {
|
||||
Write-Log $resource.Name {
|
||||
$pass = [bool](Write-Log "Test" {$resource.Test()})
|
||||
if (-not $pass) {
|
||||
Write-Log "Set" {$resource.Set()}
|
||||
$pass = [bool](Write-Log "Test" {$resource.Test()})
|
||||
if (-not $pass) {
|
||||
Write-Log "Failed"
|
||||
throw "$($resource.Name) failed to install"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Extension : Resource {
|
||||
|
||||
static [string] $ConfigurationFileName = "Extensions.json"
|
||||
static [string] $Container = "C:\Extensions"
|
||||
static [PSCustomObject] $Configuration
|
||||
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Name
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Path
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[hashtable] $BaseParameters
|
||||
|
||||
static Extension() {
|
||||
$fileName = [Extension]::ConfigurationFileName
|
||||
[Extension]::Configuration = Get-MergedConfig $fileName
|
||||
}
|
||||
|
||||
Extension([string] $name) {
|
||||
$this.Name = $name
|
||||
$this.Path = [Extension]::Container + "\$name"
|
||||
$this.BaseParameters = @{
|
||||
Service = $env:Service
|
||||
FlightingRing = $env:FlightingRing
|
||||
Region = $env:Region
|
||||
}
|
||||
$this.BaseParameters[$this.Name] = [Extension]::Configuration.($this.Name)
|
||||
}
|
||||
|
||||
[bool] Test() {
|
||||
$script = $this.Path + "\test.ps1"
|
||||
$params = $this.GetParameters($script)
|
||||
return &$script @params
|
||||
}
|
||||
|
||||
[void] Set() {
|
||||
$script = $this.Path + "\set.ps1"
|
||||
$params = $this.GetParameters($script)
|
||||
&$script @params
|
||||
}
|
||||
|
||||
[hashtable] GetParameters([string] $script) {
|
||||
$params = @{}
|
||||
$supportedParameters = (Get-Command $script).ScriptBlock.Ast.ParamBlock.Parameters.Name `
|
||||
| % {$_ -replace "\$"}
|
||||
$this.BaseParameters.Keys `
|
||||
| ? {$_ -in $supportedParameters} `
|
||||
| % {$params[$_] = $this.BaseParameters[$_]}
|
||||
return $params
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Import-Module "$PSScriptRoot\Requirements.psd1" -Force
|
||||
|
||||
$requirements = @(
|
||||
@{
|
||||
Name = "Resource 1"
|
||||
Describe = "Resource 1 is present in the system"
|
||||
Test = { $mySystem -contains 1 }
|
||||
Set = { $mySystem.Add(1) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 2"
|
||||
Describe = "Resource 2 is present in the system"
|
||||
Test = { $mySystem -contains 2 }
|
||||
Set = { $mySystem.Add(2) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 3"
|
||||
Describe = "Resource 3 is present in the system"
|
||||
Test = { $mySystem -contains 3 }
|
||||
Set = { $mySystem.Add(3) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 4"
|
||||
Describe = "Resource 4 is present in the system"
|
||||
Test = { $mySystem -contains 4 }
|
||||
Set = { $mySystem.Add(4) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 5"
|
||||
Describe = "Resource 5 is present in the system"
|
||||
Test = { $mySystem -contains 5 }
|
||||
Set = { $mySystem.Add(5) | Out-Null; Start-Sleep 1 }
|
||||
}
|
||||
)
|
||||
|
||||
# demo using Format-Table
|
||||
$mySystem = [System.Collections.ArrayList]::new()
|
||||
$requirements | Invoke-Requirement | Format-Table
|
||||
|
||||
# demo using Format-Checklist
|
||||
$mySystem = [System.Collections.ArrayList]::new()
|
||||
$requirements | Invoke-Requirement | Format-Checklist
|
||||
|
||||
# demo using Format-CallStack
|
||||
$mySystem = [System.Collections.ArrayList]::new()
|
||||
$requirements | Invoke-Requirement | Format-CallStack
|
||||
|
||||
# demo using Format-Callstack with nested requirements
|
||||
$mySystem = [System.Collections.ArrayList]::new()
|
||||
$complexRequirements = @(
|
||||
@{
|
||||
Name = "Resource 1"
|
||||
Describe = "Resource 1 is present in the system"
|
||||
Test = { $mySystem -contains 1 }
|
||||
Set = { $mySystem.Add(1) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 2"
|
||||
Describe = "Resource 2 is present in the system"
|
||||
Test = { $mySystem -contains 3 -and $mySystem -contains 4 }
|
||||
Set = {
|
||||
@(
|
||||
@{
|
||||
Name = "Resource 3"
|
||||
Describe = "Resource 3 is present in the system"
|
||||
Test = { $mySystem -contains 3 }
|
||||
Set = { $mySystem.Add(3) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 4"
|
||||
Describe = "Resource 4 is present in the system"
|
||||
Test = { $mySystem -contains 4 }
|
||||
Set = { $mySystem.Add(4) | Out-Null; Start-Sleep 1 }
|
||||
}
|
||||
) | Invoke-Requirement
|
||||
}
|
||||
},
|
||||
@{
|
||||
Name = "Resource 5"
|
||||
Describe = "Resource 5 is present in the system"
|
||||
Test = { $mySystem -contains 5 }
|
||||
Set = { $mySystem.Add(5) | Out-Null; Start-Sleep 1 }
|
||||
}
|
||||
)
|
||||
$complexRequirements | Invoke-Requirement | Format-CallStack
|
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
|
@ -0,0 +1,48 @@
|
|||
|
||||
$ErrorActionPreference = "Stop"
|
||||
."$PSScriptRoot\types.ps1"
|
||||
|
||||
# idempotently applies a requirement
|
||||
function applyRequirement([Requirement]$Requirement) {
|
||||
$result = $false
|
||||
if ($Requirement.Test) {
|
||||
[RequirementEvent]::new($Requirement, "Test", "Start")
|
||||
$result = &$Requirement.Test
|
||||
[RequirementEvent]::new($Requirement, "Test", "Stop", $result)
|
||||
}
|
||||
if (-not $result) {
|
||||
if ($Requirement.Set) {
|
||||
[RequirementEvent]::new($Requirement, "Set", "Start")
|
||||
&$Requirement.Set
|
||||
[RequirementEvent]::new($Requirement, "Set", "Stop", $result)
|
||||
}
|
||||
if ($Requirement.Test -and $Requirement.Set) {
|
||||
[RequirementEvent]::new($Requirement, "Validate", "Start")
|
||||
$result = &$Requirement.Test
|
||||
[RequirementEvent]::new($Requirement, "Validate", "Stop", $result)
|
||||
if (-not $result) {
|
||||
Write-Error "Failed to apply Requirement '$($Requirement.Name)'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# applies an array of requirements
|
||||
function applyRequirements([Requirement[]]$Requirements) {
|
||||
$Requirements | % { applyRequirement $_ }
|
||||
}
|
||||
|
||||
# sorts an array of Requirements in topological order
|
||||
function sortRequirements([Requirement[]]$Requirements) {
|
||||
$stages = @()
|
||||
while ($Requirements) {
|
||||
$nextStages = $Requirements `
|
||||
| ? { -not ($_.DependsOn | ? { $_ -notin $stages.Name }) }
|
||||
if (-not $nextStages) {
|
||||
throw "Could not resolve the dependencies for Requirements with names: $($Requirements.Name -join ', ')"
|
||||
}
|
||||
$Requirements = $Requirements | ? { $_.Name -notin $nextStages.Name }
|
||||
$stages += $nextStages
|
||||
}
|
||||
$stages
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
|
||||
."$PSScriptRoot\core.ps1"
|
||||
|
||||
Describe "Core" {
|
||||
Context "applyRequirement" {
|
||||
It "Should not 'Set' if in desired state" {
|
||||
$script:NotSetIfInDesiredState = 0
|
||||
applyRequirement @{
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $true }
|
||||
Set = { $script:NotSetIfInDesiredState++ }
|
||||
}
|
||||
$script:NotSetIfInDesiredState | Should -Be 0
|
||||
}
|
||||
It "Should 'Set' if not in desired state" {
|
||||
$script:SetIfNotInDesiredState = 0
|
||||
applyRequirement @{
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:SetIfNotInDesiredState -eq 1 }
|
||||
Set = { $script:SetIfNotInDesiredState++ }
|
||||
}
|
||||
$script:SetIfNotInDesiredState | Should -Be 1
|
||||
}
|
||||
It "Should validate once set" {
|
||||
$script:TestOnceSetIsTestCount = 0
|
||||
$script:TestOnceSetIsSet = $false
|
||||
applyRequirement @{
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:TestOnceSetIsTestCount += 1; $script:TestOnceSetIsSet }
|
||||
Set = { $script:TestOnceSetIsSet = $true }
|
||||
}
|
||||
$script:TestOnceSetIsSet | Should -Be $true
|
||||
$script:TestOnceSetIsTestCount | Should -Be 2
|
||||
}
|
||||
It "Should 'Set' if no 'Test' is provided" {
|
||||
$script:SetIfNoTest = $false
|
||||
applyRequirement @{
|
||||
Describe = "Simple Requirement"
|
||||
Set = { $script:SetIfNoTest = $true }
|
||||
}
|
||||
$script:SetIfNoTest | Should -BeTrue
|
||||
}
|
||||
It "Should not 'Test' if no 'Set' is provided" {
|
||||
$script:NotTestIfNoSet = 0
|
||||
applyRequirement @{
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:NotTestIfNoSet++ }
|
||||
}
|
||||
$script:NotTestIfNoSet | Should -Be 1
|
||||
}
|
||||
It "Should output all log events" {
|
||||
$script:SetIfNotInDesiredState = 0
|
||||
$events = applyRequirement @{
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:SetIfNotInDesiredState -eq 1 }
|
||||
Set = { $script:SetIfNotInDesiredState++ }
|
||||
}
|
||||
$expectedIds = "Test", "Set", "Validate" | % { "$_-Start", "$_-Stop" }
|
||||
$foundIds = $events | % { "$($_.Method)-$($_.State)" }
|
||||
$expectedIds | % { $_ -in $foundIds | Should -BeTrue }
|
||||
}
|
||||
}
|
||||
Context "applyRequirements" {
|
||||
It "Should call 'Test' on each requirement" {
|
||||
$script:CallTestOnEachRequirement = 0
|
||||
$requirements = 1..3 | % {
|
||||
@{
|
||||
Name = $_
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:CallTestOnEachRequirement++ % 2 }
|
||||
Set = { $false }
|
||||
}
|
||||
}
|
||||
applyRequirements $requirements
|
||||
$script:CallTestOnEachRequirement | Should -Be 6
|
||||
}
|
||||
}
|
||||
Context "sortRequirements" {
|
||||
It "Should sort an array of requirements into topological order" {
|
||||
$sorted = sortRequirements @(
|
||||
@{
|
||||
Name = "third"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
DependsOn = "first", "second"
|
||||
},
|
||||
@{
|
||||
Name = "first"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
},
|
||||
@{
|
||||
Name = "second"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
DependsOn = "first"
|
||||
}
|
||||
)
|
||||
[string[]]$names = $sorted | % Name
|
||||
0..($sorted.Count - 1) | % {
|
||||
$i, $requirement = $_, $sorted[$_]
|
||||
$requirement.DependsOn `
|
||||
| % { $names.IndexOf($_) | Should -BeLessThan $i }
|
||||
}
|
||||
}
|
||||
It "Should throw an error if there are unresolvable dependencies" {
|
||||
{
|
||||
sortRequirements @(
|
||||
@{
|
||||
Name = "third"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
DependsOn = "first", "second"
|
||||
},
|
||||
@{
|
||||
Name = "first"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
},
|
||||
@{
|
||||
Name = "second"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
DependsOn = "first", "third"
|
||||
}
|
||||
)
|
||||
} | Should -Throw
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
|
||||
using namespace System.Collections.Generic
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
."$PSScriptRoot\types.ps1"
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Formats Requirement log events as a live-updating checklist
|
||||
.NOTES
|
||||
Uses Write-Host
|
||||
#>
|
||||
function Format-Checklist {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
# Logged Requirement lifecycle events
|
||||
[Parameter(Mandatory, ValueFromPipeline)]
|
||||
[Alias("Event")]
|
||||
[RequirementEvent[]]$RequirementEvent
|
||||
)
|
||||
|
||||
begin {
|
||||
$lastDescription = ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.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 {
|
||||
$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 "$($_.Date) [$serialized] BEGIN TEST $description"
|
||||
}
|
||||
"Stop" {
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$($_.Date) [$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 "$($_.Date) [$serialized] BEGIN SET $description"
|
||||
}
|
||||
"Stop" {
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$($_.Date) [$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 "$($_.Date) [$serialized] BEGIN TEST $description"
|
||||
}
|
||||
"Stop" {
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$($_.Date) [$serialized] END TEST => $result"
|
||||
$context.Pop() | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
."$PSScriptRoot\formatters.ps1"
|
||||
|
||||
function invoke($Requirement) {
|
||||
[RequirementEvent]@{
|
||||
Requirement = $Requirement
|
||||
Method = "Test"
|
||||
State = "Start"
|
||||
}
|
||||
[RequirementEvent]@{
|
||||
Requirement = $Requirement
|
||||
Method = "Test"
|
||||
State = "Stop"
|
||||
Result = $false
|
||||
}
|
||||
[RequirementEvent]@{
|
||||
Requirement = $Requirement
|
||||
Method = "Set"
|
||||
State = "Start"
|
||||
}
|
||||
[RequirementEvent]@{
|
||||
Requirement = $Requirement
|
||||
Method = "Set"
|
||||
State = "Stop"
|
||||
Result = $null
|
||||
}
|
||||
[RequirementEvent]@{
|
||||
Requirement = $Requirement
|
||||
Method = "Validate"
|
||||
State = "Start"
|
||||
}
|
||||
[RequirementEvent]@{
|
||||
Requirement = $Requirement
|
||||
Method = "Validate"
|
||||
State = "Stop"
|
||||
Result = $true
|
||||
}
|
||||
}
|
||||
|
||||
Describe "formatters" {
|
||||
Mock Get-Date { return "00:00:00" }
|
||||
$script:InDesiredState = 0
|
||||
$requirement = @{
|
||||
Name = "simple-requirement"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:InDesiredState++ }
|
||||
Set = { }
|
||||
}
|
||||
$events = invoke $requirement
|
||||
$tempContainer = if ($env:TEMP) { $env:TEMP } else { $env:TMPDIR }
|
||||
Context "Format-Table" {
|
||||
$output = $events | Format-Table | Out-String
|
||||
It "Should print a non-empty string" {
|
||||
$output.Trim().Length | Should -BeGreaterThan 10
|
||||
}
|
||||
}
|
||||
Context "Format-Checklist" {
|
||||
$path = "$tempContainer\$(New-Guid).txt"
|
||||
$events | Format-Checklist *> $path
|
||||
$output = Get-Content $path -Raw
|
||||
Remove-Item $path
|
||||
It "Should format each line as a checklist" {
|
||||
$output | Should -Match "^\d\d:\d\d:\d\d \[ . \] Simple Requirement"
|
||||
}
|
||||
}
|
||||
Context "Format-Callstack" {
|
||||
$path = "$tempContainer\$(New-Guid).txt"
|
||||
$events | Format-CallStack *> $path
|
||||
$output = Get-Content $path -Raw
|
||||
Remove-Item $path
|
||||
It "Should format each line as a callstack" {
|
||||
$output | % { $_ | Should -Match "^\d\d:\d\d:\d\d \[.+\] .+" }
|
||||
}
|
||||
It "Should print 6 lines" {
|
||||
$output.Count | Should -Be 6
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
|
||||
$ErrorActionPreference = "Stop"
|
||||
."$PSScriptRoot\core.ps1"
|
||||
."$PSScriptRoot\formatters.ps1"
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a new Requirement object
|
||||
.OUTPUTS
|
||||
The resulting Requirement
|
||||
.NOTES
|
||||
Dsc parameter set is unsupported due to cross-platform limitations
|
||||
#>
|
||||
function New-Requirement {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
|
||||
[OutputType([Requirement])]
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
# The unique identifier for the Requirement
|
||||
[Parameter(ParameterSetName = "Script")]
|
||||
[Parameter(ParameterSetName = "Dsc")]
|
||||
[string] $Name,
|
||||
# A description of the Requirement
|
||||
[Parameter(Mandatory, ParameterSetName = "Script")]
|
||||
[Parameter(Mandatory, ParameterSetName = "Dsc")]
|
||||
[string] $Describe,
|
||||
# The Test condition that determines if the Requirement is in its desired state
|
||||
[Parameter(ParameterSetName = "Script")]
|
||||
[scriptblock] $Test,
|
||||
# The Set condition that Sets the Requirement to its desired state
|
||||
[Parameter(ParameterSetName = "Script")]
|
||||
[scriptblock] $Set,
|
||||
# The list of Requirement Names that must be in desired state prior to this Requirement
|
||||
[Parameter(ParameterSetName = "Script")]
|
||||
[Parameter(ParameterSetName = "Dsc")]
|
||||
[ValidateNotNull()]
|
||||
[string[]] $DependsOn = @(),
|
||||
# The name of the DSC resource associated with the Requirement
|
||||
[Parameter(Mandatory, ParameterSetName = "Dsc")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$ResourceName,
|
||||
# The module containing the DSC resource
|
||||
[Parameter(Mandatory, ParameterSetName = "Dsc")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$ModuleName,
|
||||
# The properties passed through to the DSC resource
|
||||
[Parameter(Mandatory, ParameterSetName = "Dsc")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[hashtable]$Property
|
||||
)
|
||||
|
||||
switch ($PSCmdlet.ParameterSetName) {
|
||||
"Script" {
|
||||
[Requirement]@{
|
||||
Name = $Name
|
||||
Describe = $Describe
|
||||
Test = $Test
|
||||
Set = $Set
|
||||
DependsOn = $DependsOn
|
||||
}
|
||||
}
|
||||
"Dsc" {
|
||||
$dscParams = @{
|
||||
Name = $ResourceName
|
||||
ModuleName = $ModuleName
|
||||
Property = $Property
|
||||
}
|
||||
[Requirement]@{
|
||||
Name = $Name
|
||||
Describe = $Describe
|
||||
Test = { Invoke-DscResource -Method "Test" @dscParams }.GetNewClosure()
|
||||
Set = { Invoke-DscResource -Method "Set" @dscParams }.GetNewClosure()
|
||||
DependsOn = $DependsOn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sets Requirements to their desired states
|
||||
.OUTPUTS
|
||||
The RequirementEvents logged from each stage of the Requirement lifecycle
|
||||
#>
|
||||
function Invoke-Requirement {
|
||||
[CmdletBinding()]
|
||||
[OutputType([RequirementEvent])]
|
||||
Param(
|
||||
# The Requirements to put in their desired state
|
||||
[Parameter(Mandatory, ValueFromPipeline)]
|
||||
[Requirement[]] $Requirement
|
||||
)
|
||||
|
||||
applyRequirements (sortRequirements $input)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests whether a requirement is in its desired state
|
||||
#>
|
||||
function Test-Requirement {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
# The Requirement to test its desired state
|
||||
[Parameter(Mandatory, ValueFromPipeline)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Requirement] $Requirement
|
||||
)
|
||||
|
||||
&$Requirement.Test
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sets the requirement to its desired state
|
||||
#>
|
||||
function Set-Requirement {
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
Param(
|
||||
# The Requirement that sets if its in its desired state
|
||||
[Parameter(Mandatory, ValueFromPipeline)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Requirement] $Requirement
|
||||
)
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($Requirement, "Set")) {
|
||||
&$Requirement.Set
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
|
||||
$ErrorActionPreference = "Stop"
|
||||
."$PSScriptRoot\interface.ps1"
|
||||
|
||||
$PlatformLacksDscSupport = $PSVersionTable.PSEdition -eq "Core"
|
||||
if (-not $PlatformLacksDscSupport) {
|
||||
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$isAdmin = $identity.groups -match "S-1-5-32-544"
|
||||
if (-not $isAdmin) {
|
||||
throw @"
|
||||
You are running PowerShell 5 and are therefore testing DSC resources.
|
||||
You must be running as admin to test DSC resources.
|
||||
"@
|
||||
}
|
||||
}
|
||||
|
||||
Describe "New-Requirement" {
|
||||
Context "'Script' parameter set" {
|
||||
$requirement = @{
|
||||
Describe = "My Requirement"
|
||||
Test = { 1 }
|
||||
Set = { 2 }
|
||||
}
|
||||
It "Should not throw" {
|
||||
{ New-Requirement @requirement } | Should -Not -Throw
|
||||
}
|
||||
It "Should not be empty" {
|
||||
New-Requirement @requirement | Should -BeTrue
|
||||
}
|
||||
}
|
||||
Context "'Dsc' parameter set" {
|
||||
It "Should not be empty" -Skip:$PlatformLacksDscSupport {
|
||||
$requirement = @{
|
||||
Describe = "My Dsc Requirement"
|
||||
ResourceName = "File"
|
||||
ModuleName = "PSDesiredStateConfiguration"
|
||||
Property = @{
|
||||
Contents = ""
|
||||
DestinationFile = ""
|
||||
}
|
||||
}
|
||||
New-Requirement @requirement | Should -BeTrue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-Requirement" {
|
||||
Context "Normal Requirement" {
|
||||
It "Should not error" {
|
||||
$requirement = @{
|
||||
Test = { 1 }
|
||||
}
|
||||
{ Invoke-Requirement $requirement } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
Context "DSC Requirement" {
|
||||
It "Should apply the DSC resource" -Skip:$PlatformLacksDscSupport {
|
||||
$tempFilePath = "$env:TEMP\_dsctest_$(New-Guid).txt"
|
||||
$content = "Hello world"
|
||||
$params = @{
|
||||
Name = "[file]MyFile"
|
||||
Describe = "My Dsc Requirement"
|
||||
ResourceName = "File"
|
||||
ModuleName = "PSDesiredStateConfiguration"
|
||||
Property = @{
|
||||
Contents = $content
|
||||
DestinationPath = $tempFilePath
|
||||
Force = $true
|
||||
}
|
||||
}
|
||||
New-Requirement @params | Invoke-Requirement
|
||||
Get-Content $tempFilePath | Should -Be $content
|
||||
Remove-Item $tempFilePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Test-Requirement" {
|
||||
It "Should not error" {
|
||||
$requirement = @{
|
||||
Test = { $true }
|
||||
}
|
||||
{ Test-Requirement $requirement } | Should -Not -Throw
|
||||
}
|
||||
It "Should pass through falsey values" {
|
||||
$requirement = @{
|
||||
Test = { $false }
|
||||
}
|
||||
Test-Requirement $requirement | Should -BeFalse
|
||||
}
|
||||
It "Should pass through truthy values" {
|
||||
$requirement = @{
|
||||
Test = { $true }
|
||||
}
|
||||
Test-Requirement $requirement | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Set-Requirement" {
|
||||
It "Should not error" {
|
||||
$requirement = @{
|
||||
Set = { $false }
|
||||
}
|
||||
{ Invoke-Requirement $requirement } | Should -Not -Throw
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
|
||||
class Requirement {
|
||||
[string] $Name
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Describe
|
||||
[scriptblock] $Test
|
||||
[scriptblock] $Set
|
||||
[string[]] $DependsOn = @()
|
||||
[string] ToString() {
|
||||
return $this.Name
|
||||
}
|
||||
}
|
||||
|
||||
enum Method {
|
||||
Test
|
||||
Set
|
||||
Validate
|
||||
}
|
||||
|
||||
enum LifecycleState {
|
||||
Start
|
||||
Stop
|
||||
}
|
||||
|
||||
class RequirementEvent {
|
||||
[datetime] $Date
|
||||
[Method] $Method
|
||||
[LifecycleState] $State
|
||||
[object] $Result
|
||||
[Requirement] $Requirement
|
||||
hidden Init([Requirement]$Requirement, [Method]$Method, [LifecycleState]$State, $Result) {
|
||||
$this.Date = Get-Date
|
||||
$this.Method = $Method
|
||||
$this.State = $State
|
||||
$this.Result = $Result
|
||||
$this.Requirement = $Requirement
|
||||
}
|
||||
RequirementEvent([Requirement]$Requirement, [Method]$Method, [LifecycleState]$State, $Result) {
|
||||
$this.Init($Requirement, $Method, $State, $Result)
|
||||
}
|
||||
RequirementEvent([Requirement]$Requirement, [Method]$Method, [LifecycleState]$State) {
|
||||
$this.Init($Requirement, $Method, $State, $null)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue