Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make InModuleScope-parameters available as variables in scriptblock #1957

Merged
merged 8 commits into from
May 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
112 changes: 92 additions & 20 deletions src/functions/InModuleScope.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
The code to be executed within the script module.
.PARAMETER Parameters
A optional hashtable of parameters to be passed to the scriptblock.
Parameters are automatically made available as variables in the scriptblock.
.PARAMETER ArgumentList
A optional list of arguments to be passed to the scriptblock.
.EXAMPLE
Expand Down Expand Up @@ -54,6 +55,52 @@
"PublicFunction". Using InModuleScope allowed this call to
"PrivateFunction" to work successfully.

.EXAMPLE
```powershell
# The script module:
function PublicFunction
{
# Does something
}

function PrivateFunction ($MyParam)
{
return $MyParam
}

Export-ModuleMember -Function PublicFunction

# The test script:

Describe 'Testing MyModule' {
BeforeAll {
Import-Module MyModule
}

It 'passing in parameter' {
$SomeVar = 123
InModuleScope 'MyModule' -Parameters @{ MyVar = $SomeVar } {
$MyVar | Should -Be 123
}
}

It 'accessing whole testcase in module socpe' -TestCases @(
@{ Name = 'Foo'; Bool = $true }
) {
# Passes the whole testcase-dictionary available in $_ to InModuleScope
InModuleScope 'MyModule' -Parameters $_ {
$Name | Should -Be 'Foo'
PrivateFunction -MyParam $Bool | Should -BeTrue
}
}
}
```

This example shows two ways of using `-Parameters` to pass variables created in a
testfile into the module scope where the scriptblock provided to InModuleScope is executed.
No variables from the outside are available inside the scriptblock without explicitly passing
them in using `-Parameters` or `-ArgumentList`.

.LINK
https://pester.dev/docs/commands/InModuleScope
#>
Expand All @@ -69,32 +116,57 @@
$ScriptBlock,

[HashTable]
$Parameters,
$Parameters = @{},

[object[]]
$ArgumentList = @()
)

$module = Get-ScriptModule -ModuleName $ModuleName -ErrorAction Stop
$sessionState = Set-SessionStateHint -PassThru -Hint "Module - $($module.Name)" -SessionState $module.SessionState

$wrapper = {
param ($private:______inmodule_parameters)

# This script block is used to create variables for provided parameters that
# the real scriptblock can inherit. Makes defining a param-block optional.

foreach ($private:______current in $private:______inmodule_parameters.Parameters.GetEnumerator()) {
$private:______inmodule_parameters.SessionState.PSVariable.Set($private:______current.Key, $private:______current.Value)
}

# Splatting expressions isn't allowed. Assigning to new private variable
$private:______arguments = $private:______inmodule_parameters.ArgumentList
$private:______parameters = $private:______inmodule_parameters.Parameters

if ($private:______parameters.Count -gt 0) {
& $private:______inmodule_parameters.ScriptBlock @private:______arguments @private:______parameters
}
else {
# Not splatting parameters to avoid polluting args
& $private:______inmodule_parameters.ScriptBlock @private:______arguments
}
}

if ($PesterPreference.Debug.WriteDebugMessages.Value) {
$hasParams = 0 -lt $Parameters.Count
$hasArgs = 0 -lt $ArgumentList.Count
$inmoduleArguments = $($(if ($hasArgs) { foreach ($a in $ArgumentList) { "'$($a)'" } }) -join ", ")
$inmoduleParameters = $(if ($hasParams) { foreach ($p in $Parameters.GetEnumerator()) { "$($p.Key) = $($p.Value)" } }) -join ", "
Write-PesterDebugMessage -Scope Runtime -Message "Running scriptblock { $scriptBlock } in module $($ModuleName)$(if ($hasParams) { " with parameters: $inmoduleParameters" })$(if ($hasArgs) { "$(if ($hasParams) { ' and' }) with arguments: $inmoduleArguments" })."
}

Set-ScriptBlockScope -ScriptBlock $ScriptBlock -SessionState $sessionState
Set-ScriptBlockScope -ScriptBlock $wrapper -SessionState $sessionState
$splat = @{
fflaten marked this conversation as resolved.
Show resolved Hide resolved
ScriptBlock = $ScriptBlock
Parameters = $Parameters
ArgumentList = $ArgumentList
SessionState = $sessionState
}

# TODO: could this simply be $PSCmdlet.SessionState? Because the original scope we are moving from
# is the scope in which this command is running, right?
# $originalState = $Pester.SessionState
# $originalScriptBlockScope = Get-ScriptBlockScope -ScriptBlock $ScriptBlock

# try {
# $sessionState = Set-SessionStateHint -PassThru -Hint "Module - $($module.Name)" -SessionState $module.SessionState
# $Pester.SessionState = $sessionState
# Set-ScriptBlockScope -ScriptBlock $ScriptBlock -SessionState $sessionState

# do {
# Write-ScriptBlockInvocationHint -Hint "InModuleScope" -ScriptBlock $ScriptBlock
& $module $ScriptBlock @Parameters @ArgumentList
# } until ($true)
# }
# finally {
# $Pester.SessionState = $originalState
# Set-ScriptBlockScope -ScriptBlock $ScriptBlock -SessionStateInternal $originalScriptBlockScope
# }
Write-ScriptBlockInvocationHint -Hint "InModuleScope" -ScriptBlock $ScriptBlock
fflaten marked this conversation as resolved.
Show resolved Hide resolved
& $wrapper $splat
}

function Get-ScriptModule {
Expand Down
135 changes: 128 additions & 7 deletions tst/functions/InModuleScope.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ Describe "Get-ScriptModule behavior" {

}

Describe 'InModuleScope parameter binding' {
# do not put this into BeforeAll this needs to be imported before calling InModuleScope
# that is below, because it requires the module to be loaded
Describe 'InModuleScope arguments and parameter binding' {

Get-Module TestModule2 | Remove-Module
New-Module -Name TestModule2 { } | Import-Module -Force
BeforeAll {
Get-Module TestModule2 | Remove-Module
New-Module -Name TestModule2 { } | Import-Module -Force
}

It 'Works with parameters while using advanced function/script' {
# https://github.com/pester/Pester/issues/1809
Expand Down Expand Up @@ -134,9 +134,130 @@ Describe 'InModuleScope parameter binding' {
InModuleScope -ModuleName TestModule2 -Parameters $inModuleScopeParameters -ScriptBlock $sb -ArgumentList $myArgs | Should -Be @($inModuleScopeParameters.SomeParam, $myArgs.Count)
}

It 'Arguments are available in scriptblock' {
$arguments = @(12345)

$sb = {
$args.Count
$args[0]
}

InModuleScope -ModuleName TestModule2 -ArgumentList $arguments -ScriptBlock $sb | Should -Be $arguments.Count, $arguments
}

It 'single argument works' {
$sb = {
$args.Count
$args[0]
}

InModuleScope -ModuleName TestModule2 -ArgumentList 'hello' -ScriptBlock $sb | Should -Be 1, 'hello'
}

It 'array argument works' {
$arguments = [int[]](1, 2, 3), 'hello'
$sb = {
$args.Count
$args[0].Count
$args[1]
}

InModuleScope -ModuleName TestModule2 -ArgumentList $arguments -ScriptBlock $sb | Should -Be 2, 3, 'hello'
}

It 'Support $null as argument' {
$sb = {
$args.Count
$args[0]
}

InModuleScope -ModuleName TestModule2 -ArgumentList $null -ScriptBlock $sb | Should -Be 1, $null
}

It 'Arguments are first in args when parameters are also used and no param-block exists' {
# https://github.com/pester/Pester/pull/1957#discussion_r637891515
$inModuleScopeParameters = @{
SomeParam = 'SomeValue'
}
$arguments = 12345

$sb = {
$args[0]
}

InModuleScope -ModuleName TestModule2 -Parameters $inModuleScopeParameters -ArgumentList $arguments -ScriptBlock $sb | Should -Be $arguments
}

It '$args is empty when no arguments are provided' {
# https://github.com/pester/Pester/pull/1957#discussion_r637772167
$sb = {
$args.Count
}

InModuleScope -ModuleName TestModule2 -ScriptBlock $sb | Should -Be 0
}

It 'Arguments bind to remaining parameters in param-block' {
$sb = {
param($param1, $param2)
$param1
$param2
$args.Count
}

InModuleScope -ModuleName TestModule2 -ScriptBlock $sb -Parameters @{ param1 = 'foo' } -ArgumentList 123 | Should -Be 'foo', 123, 0
}

It 'internal variables used in InModuleScope wrapper does not leak into scriptblock' {
$sb = {
$null -eq $SessionState
}

InModuleScope -ModuleName TestModule2 -ScriptBlock $sb | Should -BeTrue
}

It 'Automatically imports parameters as variables in module scoped scriptblock' {
# https://github.com/pester/Pester/issues/1603
$inModuleScopeParameters = @{
SomeParam2 = 'MyValue'
}

$sb = {
"$SomeParam2"
}

$sb2 = {
# Should return nothing. Making sure dynamic variable isn't persisted in module state.
"$SomeParam2"
}

InModuleScope -ModuleName TestModule2 -ScriptBlock $sb -Parameters $inModuleScopeParameters | Should -Be $inModuleScopeParameters.SomeParam2
InModuleScope -ModuleName TestModule2 -ScriptBlock $sb2 | Should -BeNullOrEmpty
}

AfterAll {
Remove-Module TestModule2 -Force
}
}

Describe "Using variables within module scope" {
BeforeAll {
Get-Module TestModule2 | Remove-Module
New-Module -Name TestModule2 { } | Import-Module -Force
}

It 'Only script-scoped variables should persist across InModuleScope calls' {
$setup = {
$script:myVar = 'bar'
$myVar2 = 'bar'
}
InModuleScope -ModuleName TestModule2 -ScriptBlock $setup

InModuleScope -ModuleName TestModule2 -ScriptBlock { $script:myVar } | Should -Be 'bar'
InModuleScope -ModuleName TestModule2 -ScriptBlock { $myVar2 } | Should -BeNullOrEmpty
}

AfterAll {
# keep this in AfterAll so we remove the module after tests are invoked
# and not while the tests are discovered
Remove-Module TestModule2 -Force
}
}