src/scriptobject/mock/MethodPatcher.ps1
# Copyright 2019, Adam Edwards # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. function MethodPatcher_Get { $patcherVariable = get-variable -scope script MethodPatcher_Singleton -erroraction ignore $patcher = if ( $patcherVariable ) { $patcherVariable.value } if ( $patcher ) { return $script:MethodPatcher_Singleton } $newPatcher = [PSCustomObject] @{ PatchedClasses = @{} Methods = @{} StaticMethodTemplate = $null NonstaticMethodTemplate = $null } $script:MethodPatcher_Singleton = $newPatcher $newPatcher $newPatcher.StaticMethodTemplate = @' {0} @args '@ $newPatcher.NonstaticMethodTemplate = @' set-strictmode -version 2 $__patchedMethod = MethodPatcher_GetPatchedMethodByFunctionName (MethodPatcher_Get) '{0}' if ( $__patchedMethod ) {{ $__objectMockScriptBlock = PatchedClassMethod_GetMockedObjectScriptBlock $__patchedMethod $this if ( $__objectMockScriptBlock ) {{ # Invoke the object-specific mock $__result = . ([ScriptBlock]::Create($__objectMockScriptBlock.tostring())) @args return $__result }} else {{ if ( ! $__patchedMethod.AllInstances ) {{ # Invoke the original unmocked method $__result = . ([ScriptBlock]::Create($__patchedMethod.OriginalScriptBlock.tostring())) @args return $__result }} }} }} # Invoke the all-instance mock of this method {0} @args '@ } function MethodPatcher_GetPatchedClass($patcher, $originalClassInfo) { $classInfo = $patcher.PatchedClasses[$originalClassInfo.classDefinition.name] if ( ! $classInfo ) { $classInfo = [ClassInfo]::New($originalClassInfo.classDefinition, $originalClassInfo.prototype, $originalClassInfo.module) } $classInfo } function MethodPatcher_SetPatchedClass($patcher, $classInfo) { $patcher.PatchedClasses[$classInfo.classDefinition.name] = $classInfo } function MethodPatcher_RemovePatchedClass($patcher, $className) { $patcher.PatchedClasses.Remove($className) } function MethodPatcher_GetPatchedMethods($patcher) { $patcher.Methods.Values } function MethodPatcher_QueryPatchedMethods($patcher, $className, $method, $staticMethods, $object) { $methodClass = $className $methodNames = if ( $method ) { @($method) } else { $classInfo = if ( $ClassName ) { MethodPatcher_GetClassDefinition $patcher $className } else { MethodPatcher_GetClassDefinition $patcher $object.scriptclass.classname } $methodClass = $classInfo.classDefinition.name if ( $staticMethods ) { $classInfo.prototype.scriptclass.psobject.methods | select -expandproperty name } else { $classInfo.classDefinition.GetInstanceMethods().name } } if ( $object ) { $methodClass = $object.scriptclass.classname } $patchedClassMethods = $methodNames | foreach { MethodPatcher_GetMockableMethodFunction $patcher $methodClass $_ $staticMethods ($object -eq $null) } $patchedClassMethods } function MethodPatcher_GetClassModule($classInfo) { $classInfo.module } function MethodPatcher_GetMockableMethodFunction( $patcher, $className, $methodName, $isStatic, $allInstances ) { $functionName = PatchedClassMethod_GetMockableMethodName $className $methodName $isStatic $existingPatchMethod = MethodPatcher_GetPatchedMethodByFunctionName $patcher $functionName if ( $existingPatchMethod ) { $existingPatchMethod } else { $classDefinition = MethodPatcher_GetClassDefinition $className $classModule = MethodPatcher_GetClassModule $classDefinition $originalMethodBlock = MethodPatcher_GetClassMethod $classDefinition $methodName $isStatic $replacementMethodBlock = MethodPatcher_CreateMethodPatchScriptBlock $patcher $functionName $isStatic $classModule $newFunc = . $classModule.NewBoundScriptBlock({param($functionName, $originalMethodBlock) new-item "function:$functionName" -value $originalMethodBlock -force ; export-modulemember -function $functionName}) $functionName $originalMethodBlock $anotherfunc = . $classModule.NewBoundScriptBlock({param([object[]] $functions) $functions | foreach { new-item "function:$($_.name)" -value $_.scriptblock -force }} ) (get-item function:MethodPatcher_Get, function:MethodPatcher_GetPatchedMethodByFunctionName, function:PatchedClassMethod_GetMockedObjectScriptBlock) $classInfo = MethodPatcher_GetPatchedClass $patcher $classDefinition $classDefinition $patchedClassMethod = PatchedClassMethod_New $classInfo $methodName $isStatic $allInstances $originalMethodBlock $replacementMethodBlock $patcher.Methods[$patchedClassMethod.FunctionName] = $patchedClassMethod $patchedClassMethod } } function MethodPatcher_CreateScriptBlockInModule($module, $block) { if ( $module ) { $module.NewBoundScriptBlock($block) } else { $block } } function MethodPatcher_GetClassDefinition($className) { $classInfo = [ClassManager]::Get().FindClassInfo($className) if ( ! $classInfo ) { throw "The specified class '$className' was not found" } $classInfo } function MethodPatcher_GetClassMethod($classDefinition, $methodName, $isStatic) { $methodBlock = if ( $isStatic ) { $classDefinition.prototype.scriptclass.psobject.methods[$methodName].script } else { $method = $classDefinition.classDefinition.GetMethod($methodName, $false) if( $method ) { $method.block } } if ( ! $methodBlock ) { throw "Method '$methodName', static='$isStatic', was not found for class '$($classDefinition.classDefinition.name)'" } $methodBlock } function MethodPatcher_CreateMethodPatchScriptBlock($patcher, $functionName, $isStatic, $module) { $newBlock = if ( $isStatic ) { [ScriptBlock]::Create($patcher.StaticMethodTemplate -f $functionName) } else { [ScriptBlock]::Create($patcher.NonstaticMethodTemplate -f $functionName) } MethodPatcher_CreateScriptBlockInModule $module $newBlock } function MethodPatcher_PatchMethod( $patcher, $className, $methodName, $isStatic, $object ) { $original = [ClassManager]::Get().GetClassInfo($className) $mockableMethod = MethodPatcher_GetMockableMethodFunction $patcher $className $methodName $isStatic ($object -eq $null) PatchedClassMethod_Patch $mockableMethod $object $newClassInfo = MethodPatcher_RegisterMethodClassInfo $mockableMethod $mockableMethod.classInfo = $newClassInfo $mockableMethod } function MethodPatcher_GetPatchedMethodByFunctionName($patcher, $functionName) { $patcher.Methods[$functionName] } function MethodPatcher_Unpatch($patcher, $patchedMethod, $object) { PatchedClassMethod_Unpatch $patchedMethod $object if ( ! ( PatchedClassMethod_IsActive $patchedMethod ) ) { . $patchedMethod.originalscriptblock.module.newboundscriptblock({param($functionname) get-item "function:$functionname" | remove-item}) $patchedMethod.functionname $restoredClassInfo = MethodPatcher_GetPatchedClass $patcher $patchedMethod.classInfo MethodPatcher_RemovePatchedClass $patcher $restoredClassInfo.classDefinition.name $patcher.Methods.Remove($patchedMethod.functionname) [ClassManager]::Get().SetClass($restoredClassInfo) } else { MethodPatcher_RegisterMethodClassInfo $patchedMethod | out-null } } function MethodPatcher_RegisterMethodClassInfo($updatedMethod) { $classContext = [ClassDefinitionContext]::new($updatedMethod.classInfo.classDefinition, $updatedMethod.classInfo.module, $updatedMethod.classInfo.prototype.scriptclass.module) $classBuilder = [ScriptClassBuilder]::new($classContext) $newClassInfo = $classBuilder.ToClassInfo($null) $newStaticPrototype = $newClassInfo.prototype.scriptclass # The updated method's class prototype is correct for instance methods # but not for static methods, so we correct this before we register # the updated class information that includes the updated method $newClassInfo.prototype = $updatedMethod.classInfo.Prototype $newClassInfo.prototype.scriptclass = $newStaticPrototype [ClassManager]::Get().SetClass($newClassInfo) $newClassInfo } |