Skip to content

Commit

Permalink
Add PSObject boxing support in PesterConfiguration (pester#1978)
Browse files Browse the repository at this point in the history
* Add PSObject boxing support in PesterConfiguration

* Removes out-of-scope changes

* Adds PS 5 plus PesterConfiguration deserializer

* Removes use of ::new in tests

* Adds tests for serializer

* Removes use of .Value on set operations

* Rewrites deserializer in CS

* Tweaks accessors

* Use SafeCommands

Co-authored-by: nohwnd <[email protected]>
  • Loading branch information
indented-automation and nohwnd authored Jun 24, 2021
1 parent 465544d commit 8634da0
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 6 deletions.
9 changes: 5 additions & 4 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,15 @@ if ($Clean) {
$null = $sbf.AppendLine("$margin``````")

$generatedLines = @($generatedConfig -split $eol)
for ($i=0; $i -lt $generatedLines.Count; $i++) {
for ($i = 0; $i -lt $generatedLines.Count; $i++) {
$l = $generatedLines[$i]
$m = if ($l) { $margin } else { $null }

if ($i -eq $generatedLines.Count-1) {
if ($i -eq $generatedLines.Count - 1) {
#last line should be blank - replace with codeblock end
$null = $sbf.AppendLine("$margin``````$eol")
} else {
}
else {
$null = $sbf.AppendLine("$m$l")
}
}
Expand Down Expand Up @@ -302,7 +303,7 @@ foreach ($f in $files) {

$sb.ToString() | Set-Content $PSScriptRoot/bin/Pester.psm1 -Encoding UTF8

$powershell = Get-Process -id $PID | Select-Object -ExpandProperty Path
$powershell = Get-Process -Id $PID | Select-Object -ExpandProperty Path

if ($Load) {
& $powershell -c "'Load: ' + (Measure-Command { Import-Module $PSScriptRoot/bin/Pester.psm1 -ErrorAction Stop}).TotalMilliseconds"
Expand Down
3 changes: 2 additions & 1 deletion src/Module.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ $script:SafeCommands['Get-MockDynamicParameter'] = $ExecutionContext.SessionStat
$script:SafeCommands['Write-PesterDebugMessage'] = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Write-PesterDebugMessage', 'function')
$script:SafeCommands['Set-DynamicParameterVariable'] = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Set-DynamicParameterVariable', 'function')


& $SafeCommands['Set-Alias'] 'Add-AssertionOperator' 'Add-ShouldOperator'
& $SafeCommands['Set-Alias'] 'Get-AssertionOperator' 'Get-ShouldOperator'

& $SafeCommands['Update-TypeData'] -TypeName PesterConfiguration -TypeConverter 'PesterConfigurationDeserializer' -SerializationDepth 5 -Force
& $SafeCommands['Update-TypeData'] -TypeName 'Deserialized.PesterConfiguration' -TargetTypeForDeserialization PesterConfiguration -Force

& $script:SafeCommands['Export-ModuleMember'] @(
'Invoke-Pester'
Expand Down
12 changes: 11 additions & 1 deletion src/csharp/Pester/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ public static T GetObjectOrNull<T>(this IDictionary dictionary, string key) wher

public static IDictionary GetIDictionaryOrNull(this IDictionary dictionary, string key)
{
return dictionary.Contains(key) ? dictionary[key] as IDictionary : null;
if (!dictionary.Contains(key))
return null;

if (dictionary[key] is PSObject)
{
return ((PSObject)dictionary[key]).BaseObject as IDictionary;
}
else
{
return dictionary[key] as IDictionary;
}
}

public static T[] GetArrayOrNull<T>(this IDictionary dictionary, string key) where T : class
Expand Down
130 changes: 130 additions & 0 deletions src/csharp/Pester/PesterConfigurationDeserializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System;
using System.Collections;
using System.Management.Automation;
using System.Reflection;
using Pester;

public class PesterConfigurationDeserializer : PSTypeConverter
{
public override bool CanConvertFrom(object sourceValue, Type destinationType)
{
if (!(sourceValue is PSObject))
return false;

return ((PSObject)sourceValue).TypeNames.Contains("Deserialized.PesterConfiguration");
}

public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
{
return ConvertToPesterConfiguration(sourceValue);
}

public override bool CanConvertTo(object sourceValue, Type destinationType)
{
throw new NotImplementedException();
}

public override object ConvertTo(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
{
throw new NotImplementedException();
}

private PesterConfiguration ConvertToPesterConfiguration(object sourceValue)
{
if (sourceValue is IDictionary)
return new PesterConfiguration((IDictionary)sourceValue);

return new PesterConfiguration(ConvertToConfigurationHashtable((PSObject)sourceValue));
}

private Hashtable ConvertToConfigurationHashtable(PSObject sourceConfiguration)
{
Hashtable configuration = new Hashtable();

foreach (var property in sourceConfiguration.Properties)
{
configuration.Add(
property.Name,
ConvertToSectionHashtable(
(PSObject)property.Value,
property.Name
)
);
}

return configuration;
}

private Hashtable ConvertToSectionHashtable(PSObject sourceSection, string sectionName)
{
Hashtable configurationSection = new Hashtable();

foreach (var property in sourceSection.Properties)
{
configurationSection.Add(
property.Name,
GetPropertyValue(
(PSObject)property.Value,
sectionName,
property.Name
)
);
}

return configurationSection;
}

private object GetPropertyValue(PSObject sourceItem, string sectionName, string propertyName)
{
var value = sourceItem.Properties["Value"].Value;

if (value is PSObject)
value = ((PSObject)value).BaseObject;

if (value == null)
return null;

var expectedType = GetExpectedType(sectionName, propertyName);

if (expectedType == typeof(ScriptBlock[]))
{
ArrayList scriptBlocks = new ArrayList();
foreach (string scriptBlock in (ArrayList)value)
{
scriptBlocks.Add(ScriptBlock.Create(scriptBlock));
}
value = scriptBlocks;
}

if (expectedType == typeof(ContainerInfo[]))
{
ArrayList containers = new ArrayList();
foreach (PSObject container in (ArrayList)value)
{
var containerInfo = Pester.ContainerInfo.Create();
containerInfo.Type = (string)container.Properties["Type"].Value;
containerInfo.Item = container.Properties["Item"].Value;
containerInfo.Data = container.Properties["Data"].Value;

containers.Add(containerInfo);
}
value = containers;
}

if (value is ArrayList)
value = ((ArrayList)value).ToArray();

return value;
}

private Type GetExpectedType(string sectionName, string propertyName)
{
return typeof(PesterConfiguration).
GetProperty(sectionName).
PropertyType.
GetProperty(propertyName).
PropertyType.
GetProperty("Value").
PropertyType;
}
}
1 change: 1 addition & 0 deletions src/functions/Pester.SafeCommands.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ $script:SafeCommands = @{
'Split-Path' = & $Get_Command -Name Split-Path -Module Microsoft.PowerShell.Management @safeCommandLookupParameters
'Start-Sleep' = & $Get_Command -Name Start-Sleep -Module Microsoft.PowerShell.Utility @safeCommandLookupParameters
'Test-Path' = & $Get_Command -Name Test-Path -Module Microsoft.PowerShell.Management @safeCommandLookupParameters
'Update-TypeData' = & $Get_Command -Name Update-TypeData -Module Microsoft.PowerShell.Utility @safeCommandLookupParameters
'Where-Object' = & $Get_Command -Name Where-Object -Module Microsoft.PowerShell.Core @safeCommandLookupParameters
'Write-Error' = & $Get_Command -Name Write-Error -Module Microsoft.PowerShell.Utility @safeCommandLookupParameters
'Write-Host' = & $Get_Command -Name Write-Host -Module Microsoft.PowerShell.Utility @safeCommandLookupParameters
Expand Down
113 changes: 113 additions & 0 deletions tst/Pester.RSpec.Configuration.ts.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,119 @@ i -PassThru:$PassThru {
$config.Run.PassThru.Value | Verify-Equal $true
$config.Filter.Tag.Value -contains 'Core' | Verify-True
}

t "Merges configuration when hashtable keys are boxed in PSObject" {
$MyOptions = @{
Run = New-Object PSObject -ArgumentList (
@{
PassThru = $true
}
)
Filter = New-Object PSObject -ArgumentList (
@{
Tag = "Core"
}
)
}

$config = New-PesterConfiguration -Hashtable $MyOptions

$config.Run.PassThru.Value | Verify-Equal $true
$config.Filter.Tag.Value -contains 'Core' | Verify-True
}

t "Merges configuration when a hashtable has been serialized" {
$BeforeSerialization = @{
Run = @{
PassThru = $true
}
Filter = @{
Tag = "Core"
}
}

$Serializer = [System.Management.Automation.PSSerializer]
$AfterSerialization = $Serializer::Deserialize($Serializer::Serialize($BeforeSerialization))
$config = New-PesterConfiguration -Hashtable $AfterSerialization

$config.Run.PassThru.Value | Verify-Equal $true
$config.Filter.Tag.Value -contains 'Core' | Verify-True
}

t "Merges configuration when a PesterConfiguration object has been serialized" {
$BeforeSerialization = New-PesterConfiguration -Hashtable @{
Run = @{
PassThru = $true
}
Filter = @{
Tag = "Core"
}
}

$Serializer = [System.Management.Automation.PSSerializer]
$AfterSerialization = $Serializer::Deserialize($Serializer::Serialize($BeforeSerialization))

$config = [PesterConfiguration]$AfterSerialization

$config.Run.PassThru.Value | Verify-Equal $true
$config.Filter.Tag.Value -contains 'Core' | Verify-True
}

t "Merges configuration when a PesterConfiguration object includes an array of values" {
$BeforeSerialization = New-PesterConfiguration -Hashtable @{
Run = @{
Path = @(
'c:\path1'
'c:\path2'
)
}
}

$Serializer = [System.Management.Automation.PSSerializer]
$AfterSerialization = $Serializer::Deserialize($Serializer::Serialize($BeforeSerialization))
$config = [PesterConfiguration]$AfterSerialization

$config.Run.Path.Value -join ',' | Verify-Equal 'c:\path1,c:\path2'
}

t "Merges configuration when a PesterConfiguration object has been serialized with a ScriptBlock" {
$BeforeSerialization = New-PesterConfiguration -Hashtable @{
Run = @{
ScriptBlock = {
'Hello world'
}
}
}

$Serializer = [System.Management.Automation.PSSerializer]
$AfterSerialization = $Serializer::Deserialize($Serializer::Serialize($BeforeSerialization))
$config = [PesterConfiguration]$AfterSerialization

$config.Run.ScriptBlock.Value.GetType() | Verify-Equal ([ScriptBlock[]])
}

t "Merges configuration when a PesterConfiguration object has been serialized with a ContainerInfo object" {
$BeforeSerialization = New-PesterConfiguration -Hashtable @{
Run = @{
Container = @(
$container = [Pester.ContainerInfo]::Create()
$container.Type = 'File'
$container.Item = 'Item'
$container.Data = 'Data'
$container
)
}
}

$Serializer = [System.Management.Automation.PSSerializer]
$AfterSerialization = $Serializer::Deserialize($Serializer::Serialize($BeforeSerialization))
$config = [PesterConfiguration]$AfterSerialization

$config.Run.Container.Value.GetType() | Verify-Equal ([Pester.ContainerInfo[]])
$config.Run.Container.Value[0].Type | Verify-Equal 'File'
$config.Run.Container.Value[0].Item | Verify-Equal 'Item'
$config.Run.Container.Value[0].Data | Verify-Equal 'Data'
}
}

b "Output.StackTraceVerbosity" {
Expand Down

0 comments on commit 8634da0

Please sign in to comment.