Skip to content

Commit

Permalink
Parametric scripts (#1671)
Browse files Browse the repository at this point in the history
Add the possibility to parametrize scripts by creating `New-TestContainer` with associated data, and passing it via `-Container` or `PesterConfiguration.Run.Container`.
  • Loading branch information
nohwnd committed Sep 4, 2020
1 parent 159205b commit fa7081d
Show file tree
Hide file tree
Showing 15 changed files with 744 additions and 40 deletions.
19 changes: 19 additions & 0 deletions src/Pester.RSpec.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,22 @@ function Remove-RSpecNonPublicProperties ($run){
$i.PluginData = $null
}
}


function New-TestContainer {
[CmdletBinding(DefaultParameterSetName="Path")]
param(
[Parameter(Mandatory, ParameterSetName = "Path")]
[String] $Path,

[Parameter(Mandatory, ParameterSetName = "ScriptBlock")]
[ScriptBlock] $ScriptBlock,

[Collections.IDictionary[]] $Data
)

switch ($PSCmdlet.ParameterSetName) {
"ScriptBlock" { [Pester.TestScriptBlock]::Create($ScriptBlock, $Data) }
Default { [Pester.TestPath]::Create($Path, $Data) }
}
}
88 changes: 68 additions & 20 deletions src/Pester.Runtime.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ function ConvertTo-ExecutedBlockContainer {

}


# endpoint for adding a block that contains tests
# or other blocks
function New-Block {
Expand All @@ -159,7 +158,8 @@ function New-Block {
[HashTable] $FrameworkData = @{ },
[Switch] $Focus,
[String] $Id,
[Switch] $Skip
[Switch] $Skip,
[Collections.IDictionary] $Data
)

# Switch-Timer -Scope Framework
Expand Down Expand Up @@ -190,6 +190,7 @@ function New-Block {
$block.Focus = $Focus
$block.Id = $Id
$block.Skip = $Skip
$block.Data = $Data

# we attach the current block to the parent, and put it to the parent
# lists
Expand Down Expand Up @@ -246,11 +247,8 @@ function Invoke-Block ($previousBlock) {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Runtime "Executing body of block '$($block.Name)'"
}
# TODO: no callbacks are provided because we are not transitioning between any states,
# it might be nice to add a parameter to indicate that we run in the same scope
# so we can avoid getting and setting the scope on scriptblock that already has that
# scope, which is _potentially_ slow because of reflection, it would also allow
# making the transition callbacks mandatory unless the parameter is provided

# no callbacks are provided because we are not transitioning between any states
$frameworkSetupResult = Invoke-ScriptBlock `
-OuterSetup @(
if ($block.First) { $state.Plugin.OneTimeBlockSetupStart }
Expand Down Expand Up @@ -945,12 +943,43 @@ function Run-Test {
throw "Teardowns are not supported in root (directly in the block container)."
}

# we add one more artificial block so the root can run
# all of it's setups and teardowns
# add OneTimeTestSetup to set variables, by having $setVariables script that will invoke in the user scope
# and $setVariablesWithContext that carries the data as is closure, this way we avoid having to provide parameters to
# before all script, but it might be better to make this a plugin, because there we can pass data.
$setVariables = {
param($private:____parameters)
foreach($private:____d in $____parameters.Data.GetEnumerator()) {
& $____parameters.Set_Variable -Name $private:____d.Name -Value $private:____d.Value
}
}

$SessionStateInternal = $script:SessionStateInternalProperty.GetValue($SessionState, $null)
$script:ScriptBlockSessionStateInternalProperty.SetValue($setVariables, $SessionStateInternal, $null)

$setVariablesAndThenRunOneTimeSetupIfAny = & {
$action = $setVariables
$setup = $rootBlock.OneTimeTestSetup
$parameters = @{
Data = $rootBlock.BlockContainer.Data
Set_Variable = $SafeCommands["Set-Variable"]
}

{
. $action $parameters
if ($null -ne $setup) {
. $setup
}
}.GetNewClosure()
}

$rootBlock.OneTimeTestSetup = $setVariablesAndThenRunOneTimeSetupIfAny

$rootBlock.ScriptBlock = {}
$SessionStateInternal = $script:SessionStateInternalProperty.GetValue($SessionState, $null)
$script:ScriptBlockSessionStateInternalProperty.SetValue($rootBlock.ScriptBlock, $SessionStateInternal, $null)

# we add one more artificial block so the root can run
# all of it's setups and teardowns
$private:parent = [Pester.Block]::Create()
$private:parent.Name = "ParentBlock"
$private:parent.Path = "Path"
Expand Down Expand Up @@ -2170,10 +2199,25 @@ function Invoke-BlockContainer {
[Management.Automation.SessionState] $SessionState
)

switch ($BlockContainer.Type) {
"ScriptBlock" { & $BlockContainer.Item }
"File" { Invoke-File -Path $BlockContainer.Item.PSPath -SessionState $SessionState }
default { throw [System.ArgumentOutOfRangeException]"" }
if ($null -ne $BlockContainer.Data -and 0 -lt $BlockContainer.Data.Count) {
foreach ($d in $BlockContainer.Data) {
switch ($BlockContainer.Type) {
"ScriptBlock" {
& $BlockContainer.Item @d
}
"File" { Invoke-File -Path $BlockContainer.Item.PSPath -SessionState $SessionState -Data $d }
default { throw [System.ArgumentOutOfRangeException]"" }
}
}
}
else {
switch ($BlockContainer.Type) {
"ScriptBlock" {
& $BlockContainer.Item
}
"File" { Invoke-File -Path $BlockContainer.Item.PSPath -SessionState $SessionState }
default { throw [System.ArgumentOutOfRangeException]"" }
}
}
}

Expand All @@ -2185,7 +2229,8 @@ function New-BlockContainerObject {
[Parameter(Mandatory, ParameterSetName = "Path")]
[String] $Path,
[Parameter(Mandatory, ParameterSetName = "File")]
[System.IO.FileInfo] $File
[System.IO.FileInfo] $File,
[Collections.IDictionary] $Data
)

$type, $item = switch ($PSCmdlet.ParameterSetName) {
Expand All @@ -2196,9 +2241,10 @@ function New-BlockContainerObject {
}

$c = [Pester.ContainerInfo]::Create()
$c.Type = $type
$c.Type = $type
$c.Item = $item
return $c
$c.Data = if ($null -ne $Data) { $Data } else { @{} }
$c
}

function New-DiscoveredBlockContainerObject {
Expand Down Expand Up @@ -2229,12 +2275,13 @@ function Invoke-File {
[String]
$Path,
[Parameter(Mandatory = $true)]
[Management.Automation.SessionState] $SessionState
[Management.Automation.SessionState] $SessionState,
[Collections.IDictionary] $Data
)

$sb = {
param ($private:p)
. $private:p
param ($private:p, $private:d)
. $private:p @d
}

# set the original session state to the wrapper scriptblock
Expand All @@ -2243,7 +2290,7 @@ function Invoke-File {
$SessionStateInternal = $script:SessionStateInternalProperty.GetValue($SessionState, $null)
$script:ScriptBlockSessionStateInternalProperty.SetValue($sb, $SessionStateInternal, $null)

& $sb $Path
& $sb $Path $Data
}

function Import-Dependency {
Expand Down Expand Up @@ -2405,6 +2452,7 @@ Export-ModuleMember -Function @(
# the core stuff I am mostly sure about
'Reset-TestSuiteState'
'New-Block'
'New-ParametrizedBlock'
'New-Test'
'New-ParametrizedTest'
'New-EachTestSetup'
Expand Down
56 changes: 49 additions & 7 deletions src/Pester.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function Add-AssertionDynamicParameterSet {

$attribute = New-Object Management.Automation.ParameterAttribute
$attribute.ParameterSetName = $AssertionEntry.Name

$attributeCollection = New-Object Collections.ObjectModel.Collection[Attribute]
$null = $attributeCollection.Add($attribute)
if (-not ([string]::IsNullOrWhiteSpace($AssertionEntry.Alias))) {
Expand Down Expand Up @@ -294,7 +294,7 @@ function Invoke-Pester {
repository, see https://github.com/Pester.
.PARAMETER CI
(Deprecated v4)
(Introduced v5)
Enable Code Coverage, Test Results and Exit after Run
Replace with ConfigurationProperty
Expand Down Expand Up @@ -470,7 +470,6 @@ function Invoke-Pester {
Note that JUnitXml is not currently supported in Pester 5.
.PARAMETER PassThru
(Deprecated v4)
Replace with ConfigurationProperty Run.PassThru
Returns a custom object (PSCustomObject) that contains the test results.
    By default, Invoke-Pester writes to the host program, not to the output stream (stdout).
Expand All @@ -479,20 +478,21 @@ function Invoke-Pester {
    To suppress the host output, use the Show parameter set to None.
.PARAMETER Path
(Deprecated v4)
Aliases Script
Specifies a test to run. The value is a path\file
    name or name pattern. Wildcards are permitted. All hash tables in a Script
    parameter values must have a Path key.
.PARAMETER PesterOption
(Deprecated v4)
    Sets advanced options for the test execution. Enter a PesterOption object,
    such as one that you create by using the New-PesterOption cmdlet, or a hash table
    in which the keys are option names and the values are option values.
    For more information on the options available, see the help for New-PesterOption.
.PARAMETER Quiet
The parameter Quiet is deprecated since Pester v. 4.0 and will be deleted
(Deprecated v4)
The parameter Quiet is deprecated since Pester v4.0 and will be deleted
    in the next major version of Pester. Please use the parameter Show
    with value 'None' instead.
    The parameter Quiet suppresses the output that Pester writes to the host program,
Expand Down Expand Up @@ -528,6 +528,7 @@ function Invoke-Pester {
    is written when you use the Output parameters.
.PARAMETER Strict
(Deprecated v4)
    Makes Pending and Skipped tests to Failed tests. Useful for continuous
    integration where you need to make sure all tests passed.
Expand Down Expand Up @@ -609,6 +610,9 @@ function Invoke-Pester {
[Parameter(ParameterSetName = "Legacy")] # Legacy set for v4 compatibility during migration - deprecated
[Switch] $PassThru,

[Parameter(ParameterSetName = "Simple")]
[Pester.TestContainer[]] $Container,

[Parameter(ParameterSetName = "Advanced")]
[PesterConfiguration] $Configuration,

Expand Down Expand Up @@ -736,6 +740,39 @@ function Invoke-Pester {

Get-Variable 'PassThru' -Scope Local | Remove-Variable
}


if ($PSBoundParameters.ContainsKey('Container')) {
# expand from the public Pester.TestContainer, or more likely Pester.TestPath types to ContainerInfo
# the public types can hold multiple sets of data, ContainerInfo can hold only one to keep the internal
# logic simple.
if ($null -ne $Container) {
$cs = @()

foreach ($c in $Container) {
$data = if ($null -eq $c.Data) { @(@{}) } else { $c.Data }
if ($c -is [Pester.TestScriptBlock]) {
foreach ($d in $data) {
$cs += New-BlockContainerObject -ScriptBlock $c.ScriptBlock -Data $d
}
}

if ($c -is [Pester.TestPath]) {
foreach ($d in $data) {
# resolve the path we are given in the same way we would resolve -Path
$files = @(Find-File -Path $c.Path -ExcludePath $PesterPreference.Run.ExcludePath.Value -Extension $PesterPreference.Run.TestExtension.Value)
foreach ($file in $files) {
$cs += New-BlockContainerObject -File $file -Data $d
}
}
}
}

$Configuration.Run.Container = $cs
}

Get-Variable 'Container' -Scope Local | Remove-Variable
}
}

if ('Legacy' -eq $PSCmdlet.ParameterSetName) {
Expand Down Expand Up @@ -1001,12 +1038,17 @@ function Invoke-Pester {
$containers += @( $PesterPreference.Run.ScriptBlock.Value | foreach { New-BlockContainerObject -ScriptBlock $_ })
}

foreach ($c in $PesterPreference.Run.Container.Value) {
$containers += $c
}

if ((any $PesterPreference.Run.Path.Value)) {
if ((none $PesterPreference.Run.ScriptBlock.Value) -or ((any $PesterPreference.Run.ScriptBlock.Value) -and '.' -ne $PesterPreference.Run.Path.Value[0])) {
if (((none $PesterPreference.Run.ScriptBlock.Value) -and (none $PesterPreference.Run.Container.Value)) -or ('.' -ne $PesterPreference.Run.Path.Value[0])) {
#TODO: Skipping the invocation when scriptblock is provided and the default path, later keep path in the default parameter set and remove scriptblock from it, so get-help still shows . as the default value and we can still provide script blocks via an advanced settings parameter
# TODO: pass the startup options as context to Start instead of just paths

$containers += @(Find-File -Path $PesterPreference.Run.Path.Value -ExcludePath $PesterPreference.Run.ExcludePath.Value -Extension $PesterPreference.Run.TestExtension.Value | foreach { New-BlockContainerObject -File $_ })
$exclusions = combineNonNull @($PesterPreference.Run.ExcludePath.Value, ($PesterPreference.Run.Container.Value | where { "File" -eq $_.Type } | foreach {$_.Item.FullName }))
$containers += @(Find-File -Path $PesterPreference.Run.Path.Value -ExcludePath $exclusions -Extension $PesterPreference.Run.TestExtension.Value | foreach { New-BlockContainerObject -File $_ })
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/Pester.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
# 'ConvertTo-JUnitReport'
'ConvertTo-Pester4Result'

# config
'New-TestContainer',

# legacy
'Assert-VerifiableMock'
'Assert-MockCalled'
Expand Down
3 changes: 3 additions & 0 deletions src/Pester.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ Set-Alias 'Get-AssertionOperator' 'Get-ShouldOperator'
'Add-ShouldOperator'
'Get-ShouldOperator'

# config
'New-TestContainer',

# export
'Export-NunitReport'
'ConvertTo-NUnitReport'
Expand Down
4 changes: 3 additions & 1 deletion src/csharp/Pester/Block.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public static Block Create()
public Block()
{
ItemType = "Block";
FrameworkData =
FrameworkData = new Hashtable();
Data = new Hashtable();
PluginData = new Hashtable();
Tests = new List<Test>();
Order = new List<object>();
Expand All @@ -25,6 +26,7 @@ public Block()

public string Name { get; set; }
public List<string> Path { get; set; }
public IDictionary Data { get; set; }
public List<Block> Blocks { get; set; } = new List<Block>();
public List<Test> Tests { get; set; } = new List<Test>();

Expand Down

0 comments on commit fa7081d

Please sign in to comment.