mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Make Select-Object/ForEach-Object/Where-Object see dynamic properties (#6898)
Dynamic (DLR) objects work in some places today, but not others. This change expands that support to ForEach-Object, Where-Object and the family of cmdlets that use 'MshExpression' (Select-Object, etc.).
This change addresses both wildcard and non-wildcard cases. In wildcard cases, it uses the existing support of generating PSDynamicMember objects for names returned by GetDynamicMemberNames.
In non-wildcard cases, a dynamic property access is attempted whether or not the name shows up in GetDynamicMemberNames, but a truly "blind" access is only attempted if we see that the base object is an IDMOP. If the dynamic access fails, you'll get the same or a similar error experience as before ("The property 'Blarg' cannot be found", or no error at all, depending on the cmdlet and the strict mode setting).
The included test coverage includes a stub for the.ForEach operator--once people are happy with this change, I can continue by adding support there.
This change should allegedly also have positive perf impact, though in actual perf testing, although it does seem ever-so-slightly faster, I found it difficult to measure much difference at all.
This commit is contained in:
committed by
Dongbo Wang
parent
3a5a3e4c6e
commit
ce3cb68551
@@ -4,9 +4,11 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using System.Management.Automation.Internal;
|
||||
using System.Management.Automation.Language;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
{
|
||||
@@ -123,13 +125,20 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
{
|
||||
// get the members first: this will expand the globbing on each parameter
|
||||
members = target.Members.Match(_stringValue,
|
||||
PSMemberTypes.Properties | PSMemberTypes.PropertySet);
|
||||
PSMemberTypes.Properties | PSMemberTypes.PropertySet | PSMemberTypes.Dynamic);
|
||||
}
|
||||
else
|
||||
{
|
||||
// we have no globbing: try an exact match, because this is quicker.
|
||||
PSMemberInfo x = target.Members[_stringValue];
|
||||
|
||||
if ((x == null) && (target.BaseObject is System.Dynamic.IDynamicMetaObjectProvider))
|
||||
{
|
||||
// We could check if GetDynamicMemberNames includes the name... but
|
||||
// GetDynamicMemberNames is only a hint, not a contract, so we'd want
|
||||
// to attempt the binding whether it's in there or not.
|
||||
x = new PSDynamicMember(_stringValue);
|
||||
}
|
||||
List<PSMemberInfo> temp = new List<PSMemberInfo>();
|
||||
if (x != null)
|
||||
{
|
||||
@@ -165,10 +174,14 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// it can be a property
|
||||
if (member is PSPropertyInfo)
|
||||
else if (member is PSPropertyInfo)
|
||||
{
|
||||
temporaryMemberList.Add(member);
|
||||
}
|
||||
// it can be a dynamic member
|
||||
else if (member is PSDynamicMember)
|
||||
{
|
||||
temporaryMemberList.Add(member);
|
||||
}
|
||||
@@ -225,11 +238,13 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
|
||||
#region Private Members
|
||||
|
||||
private CallSite<Func<CallSite, object, object>> _getValueDynamicSite;
|
||||
|
||||
private MshExpressionResult GetValue(PSObject target, bool eatExceptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
object result;
|
||||
object result = null;
|
||||
|
||||
if (Script != null)
|
||||
{
|
||||
@@ -243,12 +258,16 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
}
|
||||
else
|
||||
{
|
||||
PSMemberInfo member = target.Properties[_stringValue];
|
||||
if (member == null)
|
||||
if (_getValueDynamicSite == null)
|
||||
{
|
||||
return new MshExpressionResult(null, this, null);
|
||||
_getValueDynamicSite =
|
||||
CallSite<Func<CallSite, object, object>>.Create(
|
||||
PSGetMemberBinder.Get(
|
||||
_stringValue,
|
||||
classScope: (Type) null,
|
||||
@static: false));
|
||||
}
|
||||
result = member.Value;
|
||||
result = _getValueDynamicSite.Target.Invoke(_getValueDynamicSite, target);
|
||||
}
|
||||
|
||||
return new MshExpressionResult(result, this, null);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
@@ -16,6 +18,38 @@ using Dbg = System.Management.Automation.Diagnostics;
|
||||
|
||||
namespace Microsoft.PowerShell.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// A thin wrapper over a property-getting Callsite, to allow reuse when possible.
|
||||
/// </summary>
|
||||
struct DynamicPropertyGetter
|
||||
{
|
||||
private CallSite<Func<CallSite, object, object>> _getValueDynamicSite;
|
||||
|
||||
// For the wildcard case, lets us know if we can reuse the callsite:
|
||||
private string _lastUsedPropertyName;
|
||||
|
||||
public object GetValue(PSObject inputObject, string propertyName)
|
||||
{
|
||||
Dbg.Assert(!WildcardPattern.ContainsWildcardCharacters(propertyName), "propertyName should be pre-resolved by caller");
|
||||
|
||||
// If wildcards are involved, the resolved property name could potentially
|
||||
// be different on every object... but probably not, so we'll attempt to
|
||||
// reuse the callsite if possible.
|
||||
|
||||
if (!propertyName.Equals(_lastUsedPropertyName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_lastUsedPropertyName = propertyName;
|
||||
_getValueDynamicSite = CallSite<Func<CallSite, object, object>>.Create(
|
||||
PSGetMemberBinder.Get(
|
||||
propertyName,
|
||||
classScope: (Type) null,
|
||||
@static: false));
|
||||
}
|
||||
|
||||
return _getValueDynamicSite.Target.Invoke(_getValueDynamicSite, inputObject);
|
||||
}
|
||||
}
|
||||
|
||||
#region Built-in cmdlets that are used by or require direct access to the engine.
|
||||
|
||||
/// <summary>
|
||||
@@ -136,6 +170,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
private string _propertyOrMethodName;
|
||||
private string _targetString;
|
||||
private DynamicPropertyGetter _propGetter;
|
||||
|
||||
/// <summary>
|
||||
/// The arguments passed to a method invocation
|
||||
@@ -351,80 +386,107 @@ namespace Microsoft.PowerShell.Commands
|
||||
member = _inputObject.Members[_propertyOrMethodName];
|
||||
}
|
||||
|
||||
if (member == null)
|
||||
// member is a method
|
||||
if (member is PSMethodInfo)
|
||||
{
|
||||
errorRecord = GenerateNameParameterError("Name", InternalCommandStrings.PropertyOrMethodNotFound,
|
||||
"PropertyOrMethodNotFound", _inputObject,
|
||||
_propertyOrMethodName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// member is a method
|
||||
if (member is PSMethodInfo)
|
||||
{
|
||||
// first we check if the member is a ParameterizedProperty
|
||||
PSParameterizedProperty targetParameterizedProperty = member as PSParameterizedProperty;
|
||||
if (targetParameterizedProperty != null)
|
||||
{
|
||||
// should process
|
||||
string propertyAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectPropertyAction, targetParameterizedProperty.Name);
|
||||
|
||||
// ParameterizedProperty always take parameters, so we output the member.Value directly
|
||||
if (ShouldProcess(_targetString, propertyAction))
|
||||
{
|
||||
WriteObject(member.Value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PSMethodInfo targetMethod = member as PSMethodInfo;
|
||||
Dbg.Assert(targetMethod != null, "targetMethod should not be null here.");
|
||||
try
|
||||
{
|
||||
// should process
|
||||
string methodAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectMethodActionWithoutArguments, targetMethod.Name);
|
||||
|
||||
if (ShouldProcess(_targetString, methodAction))
|
||||
{
|
||||
if (!BlockMethodInLanguageMode(InputObject))
|
||||
{
|
||||
object result = targetMethod.Invoke(Utils.EmptyArray<object>());
|
||||
WriteToPipelineWithUnrolling(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (PipelineStoppedException)
|
||||
{
|
||||
// PipelineStoppedException can be caused by select-object
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MethodException mex = ex as MethodException;
|
||||
if (mex != null && mex.ErrorRecord != null && mex.ErrorRecord.FullyQualifiedErrorId == "MethodCountCouldNotFindBest")
|
||||
{
|
||||
WriteObject(targetMethod.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteError(new ErrorRecord(ex, "MethodInvocationError", ErrorCategory.InvalidOperation, _inputObject));
|
||||
}
|
||||
}
|
||||
}
|
||||
// member is a property
|
||||
else
|
||||
// first we check if the member is a ParameterizedProperty
|
||||
PSParameterizedProperty targetParameterizedProperty = member as PSParameterizedProperty;
|
||||
if (targetParameterizedProperty != null)
|
||||
{
|
||||
// should process
|
||||
string propertyAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectPropertyAction, member.Name);
|
||||
InternalCommandStrings.ForEachObjectPropertyAction, targetParameterizedProperty.Name);
|
||||
|
||||
// ParameterizedProperty always take parameters, so we output the member.Value directly
|
||||
if (ShouldProcess(_targetString, propertyAction))
|
||||
{
|
||||
WriteObject(member.Value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PSMethodInfo targetMethod = member as PSMethodInfo;
|
||||
Dbg.Assert(targetMethod != null, "targetMethod should not be null here.");
|
||||
try
|
||||
{
|
||||
// should process
|
||||
string methodAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectMethodActionWithoutArguments, targetMethod.Name);
|
||||
|
||||
if (ShouldProcess(_targetString, methodAction))
|
||||
{
|
||||
if (!BlockMethodInLanguageMode(InputObject))
|
||||
{
|
||||
object result = targetMethod.Invoke(Utils.EmptyArray<object>());
|
||||
WriteToPipelineWithUnrolling(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (PipelineStoppedException)
|
||||
{
|
||||
// PipelineStoppedException can be caused by select-object
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MethodException mex = ex as MethodException;
|
||||
if (mex != null && mex.ErrorRecord != null && mex.ErrorRecord.FullyQualifiedErrorId == "MethodCountCouldNotFindBest")
|
||||
{
|
||||
WriteObject(targetMethod.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteError(new ErrorRecord(ex, "MethodInvocationError", ErrorCategory.InvalidOperation, _inputObject));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string resolvedPropertyName = null;
|
||||
bool isBlindDynamicAccess = false;
|
||||
if (member == null)
|
||||
{
|
||||
if ((_inputObject.BaseObject is IDynamicMetaObjectProvider) &&
|
||||
!WildcardPattern.ContainsWildcardCharacters(_propertyOrMethodName))
|
||||
{
|
||||
// Let's just try a dynamic property access. Note that if it
|
||||
// comes to depending on dynamic access, we are assuming it is a
|
||||
// property; we don't have ETS info to tell us up front if it
|
||||
// even exists or not, let alone if it is a method or something
|
||||
// else.
|
||||
//
|
||||
// Note that this is "truly blind"--the name did not show up in
|
||||
// GetDynamicMemberNames(), else it would show up as a dynamic
|
||||
// member.
|
||||
|
||||
resolvedPropertyName = _propertyOrMethodName;
|
||||
isBlindDynamicAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorRecord = GenerateNameParameterError("Name", InternalCommandStrings.PropertyOrMethodNotFound,
|
||||
"PropertyOrMethodNotFound", _inputObject,
|
||||
_propertyOrMethodName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// member is [presumably] a property (note that it could be a
|
||||
// dynamic property, if it shows up in GetDynamicMemberNames())
|
||||
resolvedPropertyName = member.Name;
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(resolvedPropertyName))
|
||||
{
|
||||
// should process
|
||||
string propertyAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectPropertyAction, resolvedPropertyName);
|
||||
|
||||
if (ShouldProcess(_targetString, propertyAction))
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteToPipelineWithUnrolling(member.Value);
|
||||
WriteToPipelineWithUnrolling(_propGetter.GetValue(InputObject, resolvedPropertyName));
|
||||
}
|
||||
catch (TerminateException) // The debugger is terminating the execution
|
||||
{
|
||||
@@ -439,17 +501,54 @@ namespace Microsoft.PowerShell.Commands
|
||||
// PipelineStoppedException can be caused by select-object
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// When the property is not gettable or it throws an exception.
|
||||
// e.g. when trying to access an assembly's location property, since dynamic assemblies are not backed up by a file,
|
||||
// an exception will be thrown when accessing its location property. In this case, return null.
|
||||
WriteObject(null);
|
||||
// For normal property accesses, we do not generate an error
|
||||
// here. The problem for truly blind dynamic accesses (the
|
||||
// member did not show up in GetDynamicMemberNames) is that
|
||||
// we can't tell the difference between "it failed because
|
||||
// the property does not exist" (let's call this case 1) and
|
||||
// "it failed because accessing it actually threw some
|
||||
// exception" (let's call that case 2).
|
||||
//
|
||||
// PowerShell behavior for normal (non-dynamic) properties
|
||||
// is different for these two cases: case 1 gets an error
|
||||
// (which is possible because the ETS tells us up front if
|
||||
// the property exists or not), and case 2 does not. (For
|
||||
// normal properties, this catch block /is/ case 2.)
|
||||
//
|
||||
// For IDMOPs, we have the chance to attempt a "blind"
|
||||
// access, but the cost is that we must have the same
|
||||
// response to both cases (because we cannot distinguish
|
||||
// between the two). So we have to make a choice: we can
|
||||
// either swallow ALL errors (including "The property
|
||||
// 'Blarg' does not exist"), or expose them all.
|
||||
//
|
||||
// Here, for truly blind dynamic access, we choose to
|
||||
// preserve the behavior of showing "The property 'Blarg'
|
||||
// does not exist" (case 1) errors than to suppress
|
||||
// "FooException thrown when accessing Bloop property" (case
|
||||
// 2) errors.
|
||||
|
||||
if (isBlindDynamicAccess)
|
||||
{
|
||||
errorRecord = new ErrorRecord(ex,
|
||||
"DynamicPropertyAccessFailed_" + _propertyOrMethodName,
|
||||
ErrorCategory.InvalidOperation,
|
||||
InputObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
// When the property is not gettable or it throws an exception.
|
||||
// e.g. when trying to access an assembly's location property, since dynamic assemblies are not backed up by a file,
|
||||
// an exception will be thrown when accessing its location property. In this case, return null.
|
||||
WriteObject(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end of member is a property
|
||||
} // member is not null
|
||||
} // no args provided
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorRecord != null)
|
||||
{
|
||||
@@ -1452,6 +1551,8 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
}
|
||||
|
||||
private DynamicPropertyGetter _propGetter;
|
||||
|
||||
/// <summary>
|
||||
/// Execute the script block passing in the current pipeline object as
|
||||
/// it's only parameter.
|
||||
@@ -1579,6 +1680,9 @@ namespace Microsoft.PowerShell.Commands
|
||||
// has keys that can't be compared to property.
|
||||
}
|
||||
|
||||
string resolvedPropertyName = null;
|
||||
bool isBlindDynamicAccess = false;
|
||||
|
||||
ReadOnlyPSMemberInfoCollection<PSMemberInfo> members = GetMatchMembers();
|
||||
if (members.Count > 1)
|
||||
{
|
||||
@@ -1598,7 +1702,21 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
else if (members.Count == 0)
|
||||
{
|
||||
if (Context.IsStrictVersion(2))
|
||||
if ((InputObject.BaseObject is IDynamicMetaObjectProvider) &&
|
||||
!WildcardPattern.ContainsWildcardCharacters(_property))
|
||||
{
|
||||
// Let's just try a dynamic property access. Note that if it comes to
|
||||
// depending on dynamic access, we are assuming it is a property; we
|
||||
// don't have ETS info to tell us up front if it even exists or not,
|
||||
// let alone if it is a method or something else.
|
||||
//
|
||||
// Note that this is "truly blind"--the name did not show up in
|
||||
// GetDynamicMemberNames(), else it would show up as a dynamic member.
|
||||
|
||||
resolvedPropertyName = _property;
|
||||
isBlindDynamicAccess = true;
|
||||
}
|
||||
else if (Context.IsStrictVersion(2))
|
||||
{
|
||||
WriteError(ForEachObjectCommand.GenerateNameParameterError("Property",
|
||||
InternalCommandStrings.PropertyNotFound,
|
||||
@@ -1607,10 +1725,15 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPropertyName = members[0].Name;
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(resolvedPropertyName))
|
||||
{
|
||||
try
|
||||
{
|
||||
return members[0].Value;
|
||||
return _propGetter.GetValue(_inputObject, resolvedPropertyName);
|
||||
}
|
||||
catch (TerminateException)
|
||||
{
|
||||
@@ -1620,10 +1743,45 @@ namespace Microsoft.PowerShell.Commands
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// When the property is not gettable or it throws an exception
|
||||
return null;
|
||||
// For normal property accesses, we do not generate an error here. The problem
|
||||
// for truly blind dynamic accesses (the member did not show up in
|
||||
// GetDynamicMemberNames) is that we can't tell the difference between "it
|
||||
// failed because the property does not exist" (let's call this case
|
||||
// 1) and "it failed because accessing it actually threw some exception" (let's
|
||||
// call that case 2).
|
||||
//
|
||||
// PowerShell behavior for normal (non-dynamic) properties is different for
|
||||
// these two cases: case 1 gets an error (if strict mode is on) (which is
|
||||
// possible because the ETS tells us up front if the property exists or not),
|
||||
// and case 2 does not. (For normal properties, this catch block /is/ case 2.)
|
||||
//
|
||||
// For IDMOPs, we have the chance to attempt a "blind" access, but the cost is
|
||||
// that we must have the same response to both cases (because we cannot
|
||||
// distinguish between the two). So we have to make a choice: we can either
|
||||
// swallow ALL errors (including "The property 'Blarg' does not exist"), or
|
||||
// expose them all.
|
||||
//
|
||||
// Here, for truly blind dynamic access, we choose to preserve the behavior of
|
||||
// showing "The property 'Blarg' does not exist" (case 1) errors than to
|
||||
// suppress "FooException thrown when accessing Bloop property" (case
|
||||
// 2) errors.
|
||||
|
||||
if (isBlindDynamicAccess && Context.IsStrictVersion(2))
|
||||
{
|
||||
WriteError(new ErrorRecord(ex,
|
||||
"DynamicPropertyAccessFailed_" + _property,
|
||||
ErrorCategory.InvalidOperation,
|
||||
_inputObject));
|
||||
|
||||
error = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// When the property is not gettable or it throws an exception
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
Add-TestDynamicType
|
||||
|
||||
Describe "Where-Object" -Tags "CI" {
|
||||
BeforeAll {
|
||||
@@ -125,4 +126,20 @@ Describe "Where-Object" -Tags "CI" {
|
||||
$Result = $Computers | Where-Object ComputerName -match '^MGC.+'
|
||||
$Result.Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'Where-Object should handle dynamic (DLR) objects' {
|
||||
$dynObj = [TestDynamic]::new()
|
||||
$Result = $dynObj, $dynObj | Where FooProp -eq 123
|
||||
$Result.Count | Should -Be 2
|
||||
$Result[0] | Should -Be $dynObj
|
||||
$Result[1] | Should -Be $dynObj
|
||||
}
|
||||
|
||||
It 'Where-Object should handle dynamic (DLR) objects, even without property name hint' {
|
||||
$dynObj = [TestDynamic]::new()
|
||||
$Result = $dynObj, $dynObj | Where HiddenProp -eq 789
|
||||
$Result.Count | Should -Be 2
|
||||
$Result[0] | Should -Be $dynObj
|
||||
$Result[1] | Should -Be $dynObj
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
. (Join-Path -Path $PSScriptRoot -ChildPath Test-Mocks.ps1)
|
||||
Add-TestDynamicType
|
||||
|
||||
Describe "Select-Object" -Tags "CI" {
|
||||
BeforeEach {
|
||||
@@ -242,6 +243,47 @@ Describe "Select-Object DRT basic functionality" -Tags "CI" {
|
||||
$results.Count | Should -Be 1
|
||||
$results[0] | Should -BeExactly "3"
|
||||
}
|
||||
|
||||
It "Select-Object should handle dynamic (DLR) properties"{
|
||||
$dynObj = [TestDynamic]::new()
|
||||
$results = $dynObj, $dynObj | Select-Object -ExpandProperty FooProp
|
||||
$results.Count | Should -Be 2
|
||||
$results[0] | Should -Be 123
|
||||
$results[1] | Should -Be 123
|
||||
}
|
||||
|
||||
It "Select-Object should handle dynamic (DLR) properties without GetDynamicMemberNames hint"{
|
||||
$dynObj = [TestDynamic]::new()
|
||||
$results = $dynObj, $dynObj | Select-Object -ExpandProperty HiddenProp
|
||||
$results.Count | Should -Be 2
|
||||
$results[0] | Should -Be 789
|
||||
$results[1] | Should -Be 789
|
||||
}
|
||||
|
||||
It "Select-Object should handle wildcarded dynamic (DLR) properties when hinted by GetDynamicMemberNames"{
|
||||
$dynObj = [TestDynamic]::new()
|
||||
$results = $dynObj, $dynObj | Select-Object -ExpandProperty FooP*
|
||||
$results.Count | Should -Be 2
|
||||
$results[0] | Should -Be 123
|
||||
$results[1] | Should -Be 123
|
||||
}
|
||||
|
||||
It "Select-Object should work when multiple dynamic (DLR) properties match"{
|
||||
$dynObj = [TestDynamic]::new()
|
||||
$results = $dynObj, $dynObj | Select-Object *Prop
|
||||
$results.Count | Should -Be 2
|
||||
$results[0].FooProp | Should -Be 123
|
||||
$results[0].BarProp | Should -Be 456
|
||||
$results[1].FooProp | Should -Be 123
|
||||
$results[1].BarProp | Should -Be 456
|
||||
}
|
||||
|
||||
It "Select-Object -ExpandProperty should yield errors if multiple dynamic (DLR) properties match"{
|
||||
$dynObj = [TestDynamic]::new()
|
||||
$e = { $results = $dynObj, $dynObj | Select-Object -ExpandProperty *Prop -ErrorAction Stop} |
|
||||
Should -Throw -PassThru -ErrorId "MutlipleExpandProperties,Microsoft.PowerShell.Commands.SelectObjectCommand"
|
||||
$e.CategoryInfo | Should -Match "PSArgumentException"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Select-Object with Property = '*'" -Tags "CI" {
|
||||
|
||||
@@ -162,7 +162,7 @@ Describe "Adapter Tests" -tags "CI" {
|
||||
It "Common ForEach magic method tests" -Pending:$true {
|
||||
}
|
||||
|
||||
It "ForEach magic method works for singletions" {
|
||||
It "ForEach magic method works for singletons" {
|
||||
$x = 5
|
||||
$x.ForEach({$_}) | Should -Be 5
|
||||
(5).ForEach({$_}) | Should -Be 5
|
||||
@@ -187,13 +187,26 @@ Describe "Adapter Tests" -tags "CI" {
|
||||
} -PassThru -Force
|
||||
$x.ForEach(5) | Should -Be 10
|
||||
}
|
||||
|
||||
# Pending: https://github.com/PowerShell/PowerShell/issues/6567
|
||||
It "ForEach magic method works for dynamic (DLR) things" -Pending:$true {
|
||||
Add-TestDynamicType
|
||||
|
||||
$dynObj = [TestDynamic]::new()
|
||||
$results = @($dynObj, $dynObj).ForEach('FooProp')
|
||||
$results.Count | Should -Be 2
|
||||
$results[0] | Should -Be 123
|
||||
$results[1] | Should -Be 123
|
||||
|
||||
# TODO: dynamic method calls
|
||||
}
|
||||
}
|
||||
|
||||
Context "Where Magic Method Adapter Tests" {
|
||||
It "Common Where magic method tests" -Pending:$true {
|
||||
}
|
||||
|
||||
It "Where magic method works for singletions" {
|
||||
It "Where magic method works for singletons" {
|
||||
$x = 5
|
||||
$x.Where({$true}) | Should -Be 5
|
||||
(5).Where({$true}) | Should -Be 5
|
||||
|
||||
@@ -16,5 +16,5 @@ Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.'
|
||||
|
||||
Description = 'Temporary module contains functions for using in tests'
|
||||
|
||||
FunctionsToExport = 'Wait-UntilTrue', 'Test-IsElevated', 'Wait-FileToBePresent', 'Get-RandomFileName', 'Enable-Testhook', 'Disable-Testhook', 'Set-TesthookResult', 'Test-TesthookIsSet'
|
||||
FunctionsToExport = 'Wait-UntilTrue', 'Test-IsElevated', 'Wait-FileToBePresent', 'Get-RandomFileName', 'Enable-Testhook', 'Disable-Testhook', 'Set-TesthookResult', 'Test-TesthookIsSet', 'Add-TestDynamicType'
|
||||
}
|
||||
|
||||
@@ -115,3 +115,80 @@ function Set-TesthookResult
|
||||
)
|
||||
${Script:TesthookType}::SetTestHook($testhookName, $value)
|
||||
}
|
||||
|
||||
function Add-TestDynamicType
|
||||
{
|
||||
param()
|
||||
|
||||
Add-Type -TypeDefinition @'
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
|
||||
public class TestDynamic : DynamicObject
|
||||
{
|
||||
private static readonly string[] s_dynamicMemberNames = new string[] { "FooProp", "BarProp", "FooMethod", "SerialNumber" };
|
||||
|
||||
private static int s_lastSerialNumber;
|
||||
|
||||
private readonly int _serialNumber;
|
||||
|
||||
public TestDynamic()
|
||||
{
|
||||
_serialNumber = ++s_lastSerialNumber;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetDynamicMemberNames()
|
||||
{
|
||||
return s_dynamicMemberNames;
|
||||
}
|
||||
|
||||
public override bool TryGetMember(GetMemberBinder binder, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (binder.Name == "FooProp")
|
||||
{
|
||||
result = 123;
|
||||
return true;
|
||||
}
|
||||
else if (binder.Name == "BarProp")
|
||||
{
|
||||
result = 456;
|
||||
return true;
|
||||
}
|
||||
else if (binder.Name == "SerialNumber")
|
||||
{
|
||||
result = _serialNumber;
|
||||
return true;
|
||||
}
|
||||
else if (binder.Name == "HiddenProp")
|
||||
{
|
||||
// Not presented in GetDynamicMemberNames
|
||||
result = 789;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (binder.Name == "FooMethod")
|
||||
{
|
||||
result = "yes";
|
||||
return true;
|
||||
}
|
||||
else if (binder.Name == "HiddenMethod")
|
||||
{
|
||||
// Not presented in GetDynamicMemberNames
|
||||
result = _serialNumber;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user