Skip to content

Commit e73f2e3

Browse files
authored
Make module compatible with PS2 (#39)
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.
1 parent 95518ea commit e73f2e3

19 files changed

+464
-241
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ matrix:
66
osx_image: xcode9.1
77
before_install:
88
# - brew update
9-
- brew tap caskroom/cask
9+
# - brew tap caskroom/cask
1010
- brew cask install powershell
1111
- os: linux
1212
dist: trusty

Assert.psd1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@{
22

33
# Script module or binary module file associated with this manifest.
4-
RootModule = 'Assert.psm1'
4+
ModuleToProcess = 'Assert.psm1'
55

66
# Version number of this module.
77
ModuleVersion = '0.0.0'

Assert.psm1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Import-Module $PSScriptRoot/TypeClass/src/TypeClass.psm1 -DisableNameChecking
22
Import-Module $PSScriptRoot/Format/src/Format.psm1 -DisableNameChecking
33

4-
. $PSScriptRoot/Compatibility/src/New-PSObject.ps1
4+
. $PSScriptRoot/Compatibility/src/Compatibility.ps1
55

66
Get-ChildItem -Path $PSScriptRoot/src/ -Recurse -Filter *.ps1 |
77
foreach { . $_.FullName }

Compatibility/src/Compatibility.ps1

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
function New-PSObject ([hashtable]$Property) {
2+
New-Object -Type PSObject -Property $Property
3+
}
4+
5+
function Invoke-WithContext {
6+
param(
7+
[Parameter(Mandatory = $true )]
8+
[ScriptBlock] $ScriptBlock,
9+
[Parameter(Mandatory = $true)]
10+
[hashtable] $Variables)
11+
12+
# this functions is a psv2 compatible version of
13+
# ScriptBlock InvokeWithContext that is not available
14+
# in that version of PowerShell
15+
16+
# this is what the code below does
17+
# which in effect sets the context without detaching the
18+
# scriptblock from the original scope
19+
# & {
20+
# # context
21+
# $a = 10
22+
# $b = 20
23+
# # invoking our original scriptblock
24+
# & $sb
25+
# }
26+
27+
# a similar solution was $SessionState.PSVariable.Set('a', 10)
28+
# but that sets the variable for all "scopes" in the current
29+
# scope so the value persist after the original has run which
30+
# is not correct,
31+
32+
$scriptBlockWithContext = {
33+
param($context)
34+
35+
foreach ($pair in $context.Variables.GetEnumerator()) {
36+
New-Variable -Name $pair.Key -Value $pair.Value
37+
}
38+
39+
# this cleans up the variable from the session
40+
# the subexpression outputs the value of the variable
41+
# and then deletes the variable, so the value is still passed
42+
# but the variable no longer exists when the scriptblock executes
43+
& $($context.ScriptBlock; Remove-Variable -Name 'context' -Scope Local)
44+
}
45+
46+
$flags = [System.Reflection.BindingFlags]'Instance,NonPublic'
47+
$SessionState = $ScriptBlock.GetType().GetProperty("SessionState", $flags).GetValue($ScriptBlock, $null)
48+
$SessionStateInternal = $SessionState.GetType().GetProperty('Internal', $flags).GetValue($SessionState, $null)
49+
50+
# attach the original session state to the wrapper scriptblock
51+
# making it invoke in the same scope as $ScriptBlock
52+
$scriptBlockWithContext.GetType().GetProperty('SessionStateInternal', $flags).SetValue($scriptBlockWithContext, $SessionStateInternal, $null)
53+
54+
& $scriptBlockWithContext @{ ScriptBlock = $ScriptBlock; Variables = $Variables }
55+
}
56+
57+
function Test-NullOrWhiteSpace ($Value) {
58+
# psv2 compatibility, on newer .net we would simply use
59+
# [string]::isnullorwhitespace
60+
$null -eq $Value -or $Value -match "^\s*$"
61+
}
62+
63+
function Get-Type ($InputObject) {
64+
try {
65+
$ErrorActionPreference = 'Stop'
66+
# normally this would not ever throw
67+
# but in psv2 when datatable is deserialized then
68+
# [Deserialized.System.Data.DataTable] does not contain
69+
# .GetType()
70+
$InputObject.GetType()
71+
}
72+
catch [Exception] {
73+
return [Object]
74+
}
75+
76+
}

Compatibility/src/New-PSObject.ps1

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
$here = $MyInvocation.MyCommand.Path | Split-Path
2+
Import-Module -Force $here/../../Axiom/src/Axiom.psm1 -DisableNameChecking
3+
. $here/../src/Compatibility.ps1
4+
5+
Describe "New-PSObject" {
6+
It "Creates a new object of type PSCustomObject" {
7+
$hashtable = @{
8+
Name = 'Jakub'
9+
}
10+
11+
$object = New-PSObject $hashtable
12+
$object | Verify-Type ([PSCustomObject])
13+
}
14+
15+
It "Creates a new PSObject with the properties populated" {
16+
$hashtable = @{
17+
Name = 'Jakub'
18+
}
19+
20+
$object = New-PSObject $hashtable
21+
$object.Name | Verify-Equal $hashtable.Name
22+
}
23+
}
24+
25+
Describe "Test-NullOrWhiteSpace" {
26+
It "Returns `$true for `$null or whitespace" -TestCases @(
27+
@{ Value = $null }
28+
@{ Value = " " }
29+
@{ Value = " " }
30+
@{ Value = "`t" }
31+
@{ Value = "`r" }
32+
@{ Value = "`n" }
33+
@{ Value = " `t `r `n" }
34+
) {
35+
param($Value)
36+
Test-NullOrWhiteSpace $Value | Verify-True
37+
}
38+
39+
It "Returns `$false for '<value>'" -TestCases @(
40+
@{ Value = "a" }
41+
@{ Value = " abc" }
42+
@{ Value = "`tabc`t" }
43+
) {
44+
param ($Value)
45+
Test-NullOrWhiteSpace $Value | Verify-False
46+
}
47+
}
48+
49+
Describe "Invoke-WithContext" {
50+
BeforeAll {
51+
Get-Module "Test-Module" | Remove-Module
52+
$body = {
53+
$a = "in test module"
54+
$context = "context"
55+
56+
# all of these are functions returning scriptblocks
57+
# so we can test that they remain bounded to the state of
58+
# this module
59+
function sb1 { { "-$a-" } }
60+
function sb2 { { "-$a- -$b-" } }
61+
function sb3 { { "-$context- -$a-" } }
62+
}
63+
New-Module -Name "Test-Module" -ScriptBlock $body | Import-Module
64+
}
65+
66+
AfterAll {
67+
Get-Module "Test-Module" | Remove-Module
68+
}
69+
70+
It "Keeps the scriptblock attached to the original scope" {
71+
# we define variable $a here and in the module, and we must
72+
# resolve $a to the value in the module, not to the local value
73+
# or null
74+
$a = 100
75+
Invoke-WithContext -ScriptBlock (sb1) -Variables @{} |
76+
Verify-Equal "-in test module-"
77+
}
78+
79+
It "Injects variable `$b into the scope while keeping `$a attached to the module scope" {
80+
Invoke-WithContext -ScriptBlock (sb2) -Variables @{ b = 'injected' } |
81+
Verify-Equal "-in test module- -injected-"
82+
}
83+
84+
It "Does not conflict with `$Context variable that is used internally" {
85+
# internally we wrap the call in something like
86+
# & {
87+
# param($context)
88+
# & $context.ScriptBlock
89+
# }
90+
# and we need to make sure that the `$context variable
91+
# will not be seen by the original scriptblock to avoid
92+
# naming conflicts
93+
Invoke-WithContext -ScriptBlock (sb3) -Variables @{} |
94+
Verify-Equal "-context- -in test module-"
95+
}
96+
}

Compatibility/tst/New-PsObject.Tests.ps1

Lines changed: 0 additions & 22 deletions
This file was deleted.

Format/src/Format.psm1

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Import-Module $PSScriptRoot/../../TypeClass/src/TypeClass.psm1 -DisableNameChecking
2+
. $PSScriptRoot/../../Compatibility/src/Compatibility.ps1
23

34
function Format-Collection ($Value, [switch]$Pretty) {
45
$separator = ', '
@@ -13,9 +14,13 @@ function Format-Object ($Value, $Property, [switch]$Pretty) {
1314
{
1415
$Property = $Value.PSObject.Properties | Select-Object -ExpandProperty Name
1516
}
16-
$orderedProperty = $Property | Sort-Object
17+
$orderedProperty = $Property |
18+
Sort-Object |
19+
# force the values to be strings for powershell v2
20+
foreach { "$_" }
21+
1722
$valueType = Get-ShortType $Value
18-
$valueFormatted = ([string]([PSObject]$Value | Select-Object -Property $orderedProperty))
23+
$valueFormatted = [string]([PSObject]$Value | Select-Object -Property $orderedProperty)
1924

2025
if ($Pretty) {
2126
$margin = " "
@@ -77,7 +82,7 @@ function Format-Nicely ($Value, [switch]$Pretty) {
7782
return Format-Boolean -Value $Value
7883
}
7984

80-
if ($value -is [Reflection.TypeInfo])
85+
if ($value -is [type])
8186
{
8287
return Format-Type -Value $Value
8388
}
@@ -112,7 +117,7 @@ function Format-Nicely ($Value, [switch]$Pretty) {
112117
return Format-Collection -Value $Value -Pretty:$Pretty
113118
}
114119

115-
Format-Object -Value $Value -Property (Get-DisplayProperty ($Value.GetType())) -Pretty:$Pretty
120+
Format-Object -Value $Value -Property (Get-DisplayProperty (Get-Type $Value)) -Pretty:$Pretty
116121
}
117122

118123
function Get-DisplayProperty ([Type]$Type) {
@@ -140,7 +145,7 @@ function Get-DisplayProperty ([Type]$Type) {
140145
function Get-ShortType ($Value) {
141146
if ($null -ne $value)
142147
{
143-
Format-Type $Value.GetType()
148+
Format-Type (Get-Type $Value)
144149
}
145150
else
146151
{

Format/tst/Format.Tests.ps1

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
Get-Module Format | Remove-Module
2-
Import-Module $PSScriptRoot/../src/Format.psm1 -Force
2+
$here = $MyInvocation.MyCommand.Path | Split-Path
3+
Import-Module $here/../src/Format.psm1 -Force
34

4-
. $PSScriptRoot/../../Compatibility/src/New-PSObject.ps1
5+
. $here/../../Compatibility/src/Compatibility.ps1
56

6-
Add-Type -TypeDefinition 'namespace Assertions.TestType { public class Person { public string Name {get;set;} public int Age {get;set;}}}'
7+
Add-Type -TypeDefinition '
8+
namespace Assertions.TestType {
9+
public class Person {
10+
// powershell v2 mandates fully implemented properties
11+
string _name;
12+
int _age;
13+
public string Name { get { return _name; } set { _name = value; } }
14+
public int Age { get { return _age; } set { _age = value; } }
15+
}
16+
}'
717
Describe "Format-Collection" {
818
It "Formats collection of values '<value>' to '<expected>' using the default separator" -TestCases @(
919
@{ Value = (1, 2, 3); Expected = "1, 2, 3" }

TypeClass/tst/TypeClass.Tests.ps1

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
Import-Module $PSScriptRoot/../src/TypeClass.psm1 -Force
1+
$here = $MyInvocation.MyCommand.Path | Split-Path
2+
Import-Module $here/../src/TypeClass.psm1 -Force
23

34
Describe "Is-Value" {
45
It "Given '<value>', which is a value, string, enum, scriptblock or array with a single item of those types it returns `$true" -TestCases @(
@@ -119,10 +120,12 @@ Describe "Is-Collection" {
119120
It "Given a collection '<value>' of type '<type>' it returns `$true" -TestCases @(
120121
@{ Value = @() }
121122
@{ Value = 1,2,3 }
122-
@{ Value = [System.Collections.Generic.List[int]] 1 }
123-
@{ Value = [System.Collections.Generic.List[decimal]] 2 }
124-
@{ Value = [Collections.Generic.List[Int]](1,2,3) }
125-
@{ Value = [Collections.Generic.List[Int]](1,2,3) }
123+
# powershell v2 requires the coma before the number to make it
124+
# array that is convertible to a list
125+
@{ Value = [System.Collections.Generic.List[int]] ,1 }
126+
@{ Value = [System.Collections.Generic.List[decimal]] ,2 }
127+
@{ Value = [Collections.Generic.List[Int]] [int[]](1,2,3) }
128+
@{ Value = [Collections.Generic.List[Int]] [int[]](1,2,3) }
126129
# @ forces this to be an array even if there are
127130
# only 1 processes, like when you run in docker
128131
@{ Value = @(Get-Process) }

0 commit comments

Comments
 (0)