Skip to content

Instantly share code, notes, and snippets.

@nohwnd
Created September 21, 2020 13:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nohwnd/670aa06b989c826c22fab86742a1d6c6 to your computer and use it in GitHub Desktop.
Save nohwnd/670aa06b989c826c22fab86742a1d6c6 to your computer and use it in GitHub Desktop.
Mocking static property getter using Harmony
$harmony = "$PSScriptRoot/0Harmony.dll"
Import-Module $harmony
$Script:Patches = @()
function Set-StaticPropertyGetter {
param (
[Parameter(Mandatory)]
[Type] $Type,
[Parameter(Mandatory)]
[string] $PropertyName,
[Parameter(Mandatory)]
$Value
)
$property = $Type.GetProperty($PropertyName, [System.Reflection.BindingFlags]"Public, Static")
$get = $property.GetAccessors() | where { $_.Name -like "get_*" }
$fullTypeName = $Type.FullName
if (-not $get) {
throw "No public get accessor is defined for $fullTypeName.$PropertyName"
}
$dll = [IO.Path]::ChangeExtension([IO.Path]::GetTempPath(), ".dll");
$fakeTypeName = "Fake_$($fullTypeName -replace "\.","_")"
$code = @"
using HarmonyLib;
using System.Reflection;
using System.Linq;
public static class $fakeTypeName
{
private static Harmony _harmony;
public static $fullTypeName Return { get; set; }
public static MethodInfo Getter { get; set; }
public static bool Action (ref $fullTypeName __result)
{
__result = Return;
return false;
}
public static void Patch()
{
var prefix = typeof($fakeTypeName).GetMethod("Action", BindingFlags.Static | BindingFlags.Public);
_harmony = new Harmony("$fullTypeName.$PropertyName");
_harmony.Patch(Getter, new HarmonyMethod(prefix));
}
public static void Unpatch() {
_harmony.UnpatchAll();
}
}
"@
$d = Add-Type -TypeDefinition $code -ReferencedAssemblies $harmony -PassThru -ErrorAction Stop
$t = [Type]$fakeTypeName
$t::Getter = $get
$t::Return = [DateTime]::MaxValue
$t::Patch()
$script:Patches += $t
}
if ([datetime]::Now -eq [DateTime]::MaxValue) {
throw "already patched"
}
try {
Set-StaticPropertyGetter -Type ([datetime]) -PropertyName "Now" -Value [DateTime]::MaxValue
$now = [datetime]::now
if ($now -ne [datetime]::MaxValue) {
throw "Faking did not work. DateTime.Now did not return Max value, it returned '$now' instead."
}
else {
Write-Host -ForegroundColor Green "Success [DateTime]::Now returned '$now'!"
}
}
finally {
foreach ($p in $script:Patches) {
$p::Unpatch()
}
}
@nohwnd
Copy link
Author

nohwnd commented Sep 21, 2020

The same but with a killswitch to disable the mocking when we need to run our own code:

$harmony = "$PSScriptRoot/0Harmony.dll"
Import-Module $harmony

$Script:Patches = @()


function Set-StaticPropertyGetter {
    param (
        [Parameter(Mandatory)]
        [Type] $Type,
        [Parameter(Mandatory)]
        [string] $PropertyName,
        [Parameter(Mandatory)]
        $Value
    )

    $property = $Type.GetProperty($PropertyName, [System.Reflection.BindingFlags]"Public, Static")
    $get = $property.GetAccessors() | where { $_.Name -like "get_*" }

    $fullTypeName = $Type.FullName

    if (-not $get) {
        throw "No public get accessor is defined for $fullTypeName.$PropertyName"
    }

    $dll = [IO.Path]::ChangeExtension([IO.Path]::GetTempPath(), ".dll"); 
    $fakeTypeName = "Fake_$($fullTypeName -replace "\.","_")"

    $code = @"
    using HarmonyLib;
    using System.Reflection;
    using System.Linq;

    public static class $fakeTypeName
    {
        private static Harmony _harmony;
        public static $fullTypeName Return { get; set; }
        public static MethodInfo Getter { get; set; }
        public static bool Disable { get; set; }
        public static bool Action (ref $fullTypeName __result)
        {
            if (!Disable) {
                __result = Return;
                return false;  
            }
            else {
                __result = default($fullTypeName);
                return true;
            }
        } 

        public static void Patch()
        {
            var prefix = typeof($fakeTypeName).GetMethod("Action", BindingFlags.Static | BindingFlags.Public);

            _harmony = new Harmony("$fullTypeName.$PropertyName");
            _harmony.Patch(Getter, new HarmonyMethod(prefix));
        }

        public static void Unpatch() {
            _harmony.UnpatchAll();
        }
    }
"@

    $d = Add-Type -TypeDefinition $code -ReferencedAssemblies $harmony -PassThru -ErrorAction Stop
    $t = [Type]$fakeTypeName
    $t::Getter = $get
    $t::Return = [DateTime]::MaxValue
    $t::Patch()
    $script:Patches += $t
}

if ([datetime]::Now -eq [DateTime]::MaxValue) {
    throw "already patched"
}

try {


    Set-StaticPropertyGetter -Type ([datetime]) -PropertyName "Now" -Value [DateTime]::MaxValue
    foreach ($f in $patches) { 
        $f::Disable = $false
    }
    $now = [datetime]::now
    if ($now -ne [datetime]::MaxValue) {
        throw "Faking did not work. DateTime.Now did not return Max value, it returned '$now' instead."
    }
    else { 
        Write-Host -ForegroundColor Green "Success [DateTime]::Now returned '$now'!"
    }

    foreach ($f in $patches) { 
        $f::Disable = $true
    }

    $now = [datetime]::now
    if ($now -eq [datetime]::MaxValue) {
        throw "Disabling faking did not work. DateTime.Now returned Max value."
    }
    else { 
        Write-Host -ForegroundColor Green "Success [DateTime]::Now returned '$now'!"
    }
}
finally { 
    foreach ($p in $script:Patches) {
        $p::Unpatch()
    }
}

@nohwnd
Copy link
Author

nohwnd commented Sep 23, 2020

few bugs in the above code, I am not passing $Value, and the return type should not the the same as the source type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment