Skip to content

Commit

Permalink
Make module compatible with PS2 (#39)
Browse files Browse the repository at this point in the history
Make the module compatible with Windows PowerShell 2. The problems are anottated in the code so we can remove them when migrating to newer version of PowerShell.
  • Loading branch information
nohwnd authored Dec 9, 2018
1 parent 95518ea commit e73f2e3
Show file tree
Hide file tree
Showing 19 changed files with 464 additions and 241 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ matrix:
osx_image: xcode9.1
before_install:
# - brew update
- brew tap caskroom/cask
# - brew tap caskroom/cask
- brew cask install powershell
- os: linux
dist: trusty
Expand Down
2 changes: 1 addition & 1 deletion Assert.psd1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@{

# Script module or binary module file associated with this manifest.
RootModule = 'Assert.psm1'
ModuleToProcess = 'Assert.psm1'

# Version number of this module.
ModuleVersion = '0.0.0'
Expand Down
2 changes: 1 addition & 1 deletion Assert.psm1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Import-Module $PSScriptRoot/TypeClass/src/TypeClass.psm1 -DisableNameChecking
Import-Module $PSScriptRoot/Format/src/Format.psm1 -DisableNameChecking

. $PSScriptRoot/Compatibility/src/New-PSObject.ps1
. $PSScriptRoot/Compatibility/src/Compatibility.ps1

Get-ChildItem -Path $PSScriptRoot/src/ -Recurse -Filter *.ps1 |
foreach { . $_.FullName }
Expand Down
76 changes: 76 additions & 0 deletions Compatibility/src/Compatibility.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
function New-PSObject ([hashtable]$Property) {
New-Object -Type PSObject -Property $Property
}

function Invoke-WithContext {
param(
[Parameter(Mandatory = $true )]
[ScriptBlock] $ScriptBlock,
[Parameter(Mandatory = $true)]
[hashtable] $Variables)

# this functions is a psv2 compatible version of
# ScriptBlock InvokeWithContext that is not available
# in that version of PowerShell

# this is what the code below does
# which in effect sets the context without detaching the
# scriptblock from the original scope
# & {
# # context
# $a = 10
# $b = 20
# # invoking our original scriptblock
# & $sb
# }

# a similar solution was $SessionState.PSVariable.Set('a', 10)
# but that sets the variable for all "scopes" in the current
# scope so the value persist after the original has run which
# is not correct,

$scriptBlockWithContext = {
param($context)

foreach ($pair in $context.Variables.GetEnumerator()) {
New-Variable -Name $pair.Key -Value $pair.Value
}

# this cleans up the variable from the session
# the subexpression outputs the value of the variable
# and then deletes the variable, so the value is still passed
# but the variable no longer exists when the scriptblock executes
& $($context.ScriptBlock; Remove-Variable -Name 'context' -Scope Local)
}

$flags = [System.Reflection.BindingFlags]'Instance,NonPublic'
$SessionState = $ScriptBlock.GetType().GetProperty("SessionState", $flags).GetValue($ScriptBlock, $null)
$SessionStateInternal = $SessionState.GetType().GetProperty('Internal', $flags).GetValue($SessionState, $null)

# attach the original session state to the wrapper scriptblock
# making it invoke in the same scope as $ScriptBlock
$scriptBlockWithContext.GetType().GetProperty('SessionStateInternal', $flags).SetValue($scriptBlockWithContext, $SessionStateInternal, $null)

& $scriptBlockWithContext @{ ScriptBlock = $ScriptBlock; Variables = $Variables }
}

function Test-NullOrWhiteSpace ($Value) {
# psv2 compatibility, on newer .net we would simply use
# [string]::isnullorwhitespace
$null -eq $Value -or $Value -match "^\s*$"
}

function Get-Type ($InputObject) {
try {
$ErrorActionPreference = 'Stop'
# normally this would not ever throw
# but in psv2 when datatable is deserialized then
# [Deserialized.System.Data.DataTable] does not contain
# .GetType()
$InputObject.GetType()
}
catch [Exception] {
return [Object]
}

}
3 changes: 0 additions & 3 deletions Compatibility/src/New-PSObject.ps1

This file was deleted.

96 changes: 96 additions & 0 deletions Compatibility/tst/Compatibility.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
$here = $MyInvocation.MyCommand.Path | Split-Path
Import-Module -Force $here/../../Axiom/src/Axiom.psm1 -DisableNameChecking
. $here/../src/Compatibility.ps1

Describe "New-PSObject" {
It "Creates a new object of type PSCustomObject" {
$hashtable = @{
Name = 'Jakub'
}

$object = New-PSObject $hashtable
$object | Verify-Type ([PSCustomObject])
}

It "Creates a new PSObject with the properties populated" {
$hashtable = @{
Name = 'Jakub'
}

$object = New-PSObject $hashtable
$object.Name | Verify-Equal $hashtable.Name
}
}

Describe "Test-NullOrWhiteSpace" {
It "Returns `$true for `$null or whitespace" -TestCases @(
@{ Value = $null }
@{ Value = " " }
@{ Value = " " }
@{ Value = "`t" }
@{ Value = "`r" }
@{ Value = "`n" }
@{ Value = " `t `r `n" }
) {
param($Value)
Test-NullOrWhiteSpace $Value | Verify-True
}

It "Returns `$false for '<value>'" -TestCases @(
@{ Value = "a" }
@{ Value = " abc" }
@{ Value = "`tabc`t" }
) {
param ($Value)
Test-NullOrWhiteSpace $Value | Verify-False
}
}

Describe "Invoke-WithContext" {
BeforeAll {
Get-Module "Test-Module" | Remove-Module
$body = {
$a = "in test module"
$context = "context"

# all of these are functions returning scriptblocks
# so we can test that they remain bounded to the state of
# this module
function sb1 { { "-$a-" } }
function sb2 { { "-$a- -$b-" } }
function sb3 { { "-$context- -$a-" } }
}
New-Module -Name "Test-Module" -ScriptBlock $body | Import-Module
}

AfterAll {
Get-Module "Test-Module" | Remove-Module
}

It "Keeps the scriptblock attached to the original scope" {
# we define variable $a here and in the module, and we must
# resolve $a to the value in the module, not to the local value
# or null
$a = 100
Invoke-WithContext -ScriptBlock (sb1) -Variables @{} |
Verify-Equal "-in test module-"
}

It "Injects variable `$b into the scope while keeping `$a attached to the module scope" {
Invoke-WithContext -ScriptBlock (sb2) -Variables @{ b = 'injected' } |
Verify-Equal "-in test module- -injected-"
}

It "Does not conflict with `$Context variable that is used internally" {
# internally we wrap the call in something like
# & {
# param($context)
# & $context.ScriptBlock
# }
# and we need to make sure that the `$context variable
# will not be seen by the original scriptblock to avoid
# naming conflicts
Invoke-WithContext -ScriptBlock (sb3) -Variables @{} |
Verify-Equal "-context- -in test module-"
}
}
22 changes: 0 additions & 22 deletions Compatibility/tst/New-PsObject.Tests.ps1

This file was deleted.

15 changes: 10 additions & 5 deletions Format/src/Format.psm1
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Import-Module $PSScriptRoot/../../TypeClass/src/TypeClass.psm1 -DisableNameChecking
. $PSScriptRoot/../../Compatibility/src/Compatibility.ps1

function Format-Collection ($Value, [switch]$Pretty) {
$separator = ', '
Expand All @@ -13,9 +14,13 @@ function Format-Object ($Value, $Property, [switch]$Pretty) {
{
$Property = $Value.PSObject.Properties | Select-Object -ExpandProperty Name
}
$orderedProperty = $Property | Sort-Object
$orderedProperty = $Property |
Sort-Object |
# force the values to be strings for powershell v2
foreach { "$_" }

$valueType = Get-ShortType $Value
$valueFormatted = ([string]([PSObject]$Value | Select-Object -Property $orderedProperty))
$valueFormatted = [string]([PSObject]$Value | Select-Object -Property $orderedProperty)

if ($Pretty) {
$margin = " "
Expand Down Expand Up @@ -77,7 +82,7 @@ function Format-Nicely ($Value, [switch]$Pretty) {
return Format-Boolean -Value $Value
}

if ($value -is [Reflection.TypeInfo])
if ($value -is [type])
{
return Format-Type -Value $Value
}
Expand Down Expand Up @@ -112,7 +117,7 @@ function Format-Nicely ($Value, [switch]$Pretty) {
return Format-Collection -Value $Value -Pretty:$Pretty
}

Format-Object -Value $Value -Property (Get-DisplayProperty ($Value.GetType())) -Pretty:$Pretty
Format-Object -Value $Value -Property (Get-DisplayProperty (Get-Type $Value)) -Pretty:$Pretty
}

function Get-DisplayProperty ([Type]$Type) {
Expand Down Expand Up @@ -140,7 +145,7 @@ function Get-DisplayProperty ([Type]$Type) {
function Get-ShortType ($Value) {
if ($null -ne $value)
{
Format-Type $Value.GetType()
Format-Type (Get-Type $Value)
}
else
{
Expand Down
16 changes: 13 additions & 3 deletions Format/tst/Format.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
Get-Module Format | Remove-Module
Import-Module $PSScriptRoot/../src/Format.psm1 -Force
$here = $MyInvocation.MyCommand.Path | Split-Path
Import-Module $here/../src/Format.psm1 -Force

. $PSScriptRoot/../../Compatibility/src/New-PSObject.ps1
. $here/../../Compatibility/src/Compatibility.ps1

Add-Type -TypeDefinition 'namespace Assertions.TestType { public class Person { public string Name {get;set;} public int Age {get;set;}}}'
Add-Type -TypeDefinition '
namespace Assertions.TestType {
public class Person {
// powershell v2 mandates fully implemented properties
string _name;
int _age;
public string Name { get { return _name; } set { _name = value; } }
public int Age { get { return _age; } set { _age = value; } }
}
}'
Describe "Format-Collection" {
It "Formats collection of values '<value>' to '<expected>' using the default separator" -TestCases @(
@{ Value = (1, 2, 3); Expected = "1, 2, 3" }
Expand Down
13 changes: 8 additions & 5 deletions TypeClass/tst/TypeClass.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Import-Module $PSScriptRoot/../src/TypeClass.psm1 -Force
$here = $MyInvocation.MyCommand.Path | Split-Path
Import-Module $here/../src/TypeClass.psm1 -Force

Describe "Is-Value" {
It "Given '<value>', which is a value, string, enum, scriptblock or array with a single item of those types it returns `$true" -TestCases @(
Expand Down Expand Up @@ -119,10 +120,12 @@ Describe "Is-Collection" {
It "Given a collection '<value>' of type '<type>' it returns `$true" -TestCases @(
@{ Value = @() }
@{ Value = 1,2,3 }
@{ Value = [System.Collections.Generic.List[int]] 1 }
@{ Value = [System.Collections.Generic.List[decimal]] 2 }
@{ Value = [Collections.Generic.List[Int]](1,2,3) }
@{ Value = [Collections.Generic.List[Int]](1,2,3) }
# powershell v2 requires the coma before the number to make it
# array that is convertible to a list
@{ Value = [System.Collections.Generic.List[int]] ,1 }
@{ Value = [System.Collections.Generic.List[decimal]] ,2 }
@{ Value = [Collections.Generic.List[Int]] [int[]](1,2,3) }
@{ Value = [Collections.Generic.List[Int]] [int[]](1,2,3) }
# @ forces this to be an array even if there are
# only 1 processes, like when you run in docker
@{ Value = @(Get-Process) }
Expand Down
11 changes: 9 additions & 2 deletions src/Collection/Assert-All.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
function Assert-All {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline=$true, Position=1)]
$Actual,
Expand All @@ -14,8 +15,14 @@ function Assert-All {
# simply using '&' won't work
# see: https://blogs.msdn.microsoft.com/sergey_babkins_blog/2014/10/30/calling-the-script-blocks-in-powershell/
$actualFiltered = $Actual | foreach {
$underscore = Get-Variable _
$pass = $FilterScript.InvokeWithContext($null, $underscore, $null)
# powershell v4 code where we have InvokeWithContext available
# $underscore = Get-Variable _
# $pass = $FilterScript.InvokeWithContext($null, $underscore, $null)

# polyfill for PowerShell v2
$PSCmdlet.SessionState.PSVariable.Set("_", $_)
$pass = & $FilterScript


if (-not $pass) { $_ }
}
Expand Down
3 changes: 2 additions & 1 deletion src/Common/Add-AssertionException.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
$typeDefinition = Get-Content $PSScriptRoot/AssertionException.cs | Out-String
$here = $MyInvocation.MyCommand.Path | Split-Path
$typeDefinition = Get-Content $here/AssertionException.cs | Out-String
Add-Type -TypeDefinition $typeDefinition -WarningAction SilentlyContinue
Loading

0 comments on commit e73f2e3

Please sign in to comment.