Some design changes, greening and overhaul of tests
* Using new test-filesystem fixture to fully isolate dll's from the rest of the tooling - we should only have the test dll, the system(s) under test and their dependencies, and the mock types dll. * Ensuring that the mock types dll is available to whatever we're patching, for peverify and any other tooling that can't be instructed where to find it * Fixed messy and confusing list of assembly paths passed in to the patcher. Will do a more explicit and different way in future..probably an exclusion list or maybe a filter callback. * Small improvements like switching to NiceIO where possible, using flags enum for options, tuning PE verification, simplifying mock type management, breaking out methods, reusing utilities, fixing bugs along the way
This commit is contained in:
Родитель
bd47b10bc6
Коммит
1d3cd92503
|
@ -1,8 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using NiceIO;
|
||||||
using NSubstitute.Core;
|
using NSubstitute.Core;
|
||||||
using NSubstitute.Core.Arguments;
|
using NSubstitute.Core.Arguments;
|
||||||
using NSubstitute.Elevated.Weaver;
|
using NSubstitute.Elevated.Weaver;
|
||||||
|
@ -31,13 +31,16 @@ namespace NSubstitute.Elevated
|
||||||
new ElevatedCallRouterFactory(), ElevatedSubstituteManager, new CallRouterResolver());
|
new ElevatedCallRouterFactory(), ElevatedSubstituteManager, new CallRouterResolver());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IDisposable AutoHook(string assemblyLocation, IEnumerable<string> assemblyPath = null)
|
public static IDisposable AutoHook(string assemblyLocation)
|
||||||
{
|
{
|
||||||
var hookedContext = SubstitutionContext.Current;
|
var hookedContext = SubstitutionContext.Current;
|
||||||
var thisContext = new ElevatedSubstitutionContext(hookedContext);
|
var thisContext = new ElevatedSubstitutionContext(hookedContext);
|
||||||
SubstitutionContext.Current = thisContext;
|
SubstitutionContext.Current = thisContext;
|
||||||
|
|
||||||
var patchAllDependentAssemblies = ElevatedWeaver.PatchAllDependentAssemblies(assemblyLocation, PatchTestAssembly.Yes, assemblyPath).ToList();
|
// TODO: return a new IDisposable class that also contains the list of patch results, then in caller verify that against expected (don't want to go too wide)
|
||||||
|
|
||||||
|
var patchAllDependentAssemblies = ElevatedWeaver.PatchAllDependentAssemblies(
|
||||||
|
new NPath(assemblyLocation), PatchOptions.PatchTestAssembly).ToList();
|
||||||
|
|
||||||
return new DelegateDisposable(() =>
|
return new DelegateDisposable(() =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,12 +6,17 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Mono.Cecil;
|
using Mono.Cecil;
|
||||||
using Shouldly;
|
using NiceIO;
|
||||||
using Unity.Core;
|
using Unity.Core;
|
||||||
|
|
||||||
namespace NSubstitute.Elevated.Weaver
|
namespace NSubstitute.Elevated.Weaver
|
||||||
{
|
{
|
||||||
public enum PatchTestAssembly { No, Yes }
|
[Flags]
|
||||||
|
public enum PatchOptions
|
||||||
|
{
|
||||||
|
PatchTestAssembly = 1 << 0, // typically we don't want to patch the test assembly itself, only the systems under test
|
||||||
|
SkipPeVerify = 1 << 1, // maybe flip this bit the other way when we get a really solid weaver (peverify has an obvious perf cost)
|
||||||
|
}
|
||||||
|
|
||||||
public static class ElevatedWeaver
|
public static class ElevatedWeaver
|
||||||
{
|
{
|
||||||
|
@ -20,122 +25,112 @@ namespace NSubstitute.Elevated.Weaver
|
||||||
public static string GetPatchBackupPathFor(string path)
|
public static string GetPatchBackupPathFor(string path)
|
||||||
=> path + k_PatchBackupExtension;
|
=> path + k_PatchBackupExtension;
|
||||||
|
|
||||||
public static IReadOnlyCollection<PatchResult> PatchAllDependentAssemblies([NotNull] string testAssemblyPath,
|
public static IReadOnlyCollection<PatchResult> PatchAllDependentAssemblies(NPath testAssemblyPath, PatchOptions patchOptions)
|
||||||
PatchTestAssembly patchTestAssembly = PatchTestAssembly.No, IEnumerable<string> assemblyPath = null) // typically we don't want to patch the test assembly itself, only the systems under test
|
|
||||||
{
|
{
|
||||||
var testAssemblyFolder = Path.GetDirectoryName(testAssemblyPath);
|
// TODO: ensure we do not have any assemblies that we want to patch already loaded
|
||||||
if (testAssemblyFolder.IsNullOrEmpty())
|
// (this will require the separate in-memory patching ability)
|
||||||
throw new Exception("Unable to find folder for test assembly");
|
|
||||||
testAssemblyFolder = Path.GetFullPath(testAssemblyFolder);
|
|
||||||
|
|
||||||
// scope
|
// this dll has types we're going to be injecting, so ensure it is in the same folder
|
||||||
|
//var targetWeaverDll
|
||||||
|
|
||||||
|
var toProcess = new List<NPath> { testAssemblyPath.FileMustExist() };
|
||||||
|
var patchResults = new Dictionary<string, PatchResult>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var mockInjector = new MockInjector();
|
||||||
|
|
||||||
|
EnsureMockTypesInFolder(testAssemblyPath.Parent);
|
||||||
|
|
||||||
|
for (var toProcessIndex = 0; toProcessIndex < toProcess.Count; ++toProcessIndex)
|
||||||
{
|
{
|
||||||
var thisAssemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
var assemblyToPatchPath = toProcess[toProcessIndex];
|
||||||
if (thisAssemblyFolder.IsNullOrEmpty())
|
|
||||||
throw new Exception("Can only patch assemblies on disk");
|
|
||||||
thisAssemblyFolder = Path.GetFullPath(thisAssemblyFolder);
|
|
||||||
|
|
||||||
// keep things really simple, at least for now
|
// as we accumulate dependencies recursively, we will probably get some duplicates we can early-out on
|
||||||
if (string.Compare(testAssemblyFolder, thisAssemblyFolder, StringComparison.OrdinalIgnoreCase) != 0)
|
if (patchResults.ContainsKey(assemblyToPatchPath))
|
||||||
throw new Exception("All assemblies must be in the same folder");
|
continue;
|
||||||
|
|
||||||
|
using (var assemblyToPatch = AssemblyDefinition.ReadAssembly(assemblyToPatchPath))
|
||||||
|
{
|
||||||
|
GatherReferences(assemblyToPatchPath, assemblyToPatch);
|
||||||
|
TryPatch(assemblyToPatchPath, assemblyToPatch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var nsubElevatedPath = Path.Combine(testAssemblyFolder, "NSubstitute.Elevated.dll");
|
void GatherReferences(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch)
|
||||||
using (var nsubElevatedAssembly = AssemblyDefinition.ReadAssembly(nsubElevatedPath))
|
|
||||||
{
|
{
|
||||||
var mockInjector = new MockInjector(nsubElevatedAssembly);
|
foreach (var referencedAssembly in assemblyToPatch.Modules.SelectMany(m => m.AssemblyReferences))
|
||||||
var toProcess = new List<string> { Path.GetFullPath(testAssemblyPath) };
|
|
||||||
var patchResults = new Dictionary<string, PatchResult>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
for (var toProcessIndex = 0; toProcessIndex < toProcess.Count; ++toProcessIndex)
|
|
||||||
{
|
{
|
||||||
var assemblyToPatchPath = toProcess[toProcessIndex];
|
// only patch dll's we "own", that are in the same folder as the test assembly
|
||||||
if (patchResults.ContainsKey(assemblyToPatchPath))
|
var referencedAssemblyPath = assemblyToPatchPath.Parent.Combine(referencedAssembly.Name + ".dll");
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!Path.IsPathRooted(assemblyToPatchPath))
|
if (referencedAssemblyPath.FileExists())
|
||||||
throw new Exception($"Unexpected non-rooted assembly path '{assemblyToPatchPath}'");
|
toProcess.Add(referencedAssemblyPath);
|
||||||
|
else if (!patchResults.ContainsKey(referencedAssembly.Name))
|
||||||
|
patchResults.Add(referencedAssembly.Name, new PatchResult(referencedAssembly.Name, null, PatchState.IgnoredOutsideAllowedPaths));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
using (var assemblyToPatch = AssemblyDefinition.ReadAssembly(assemblyToPatchPath))
|
void TryPatch(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch)
|
||||||
{
|
{
|
||||||
foreach (var referencedAssembly in assemblyToPatch.Modules.SelectMany(m => m.AssemblyReferences))
|
PatchResult patchResult;
|
||||||
{
|
|
||||||
// only patch dll's we "own", that are in the same folder as the test assembly
|
|
||||||
var foundPath = Path.Combine(testAssemblyFolder, referencedAssembly.Name + ".dll");
|
|
||||||
|
|
||||||
if (File.Exists(foundPath))
|
var alreadyPatched = MockInjector.IsPatched(assemblyToPatch);
|
||||||
toProcess.Add(foundPath);
|
|
||||||
else if (!patchResults.ContainsKey(referencedAssembly.Name))
|
|
||||||
patchResults.Add(referencedAssembly.Name, new PatchResult(referencedAssembly.Name, null, PatchState.IgnoredOutsideAllowedPaths));
|
|
||||||
}
|
|
||||||
|
|
||||||
PatchResult patchResult;
|
if (assemblyToPatchPath == testAssemblyPath && (patchOptions & PatchOptions.PatchTestAssembly) == 0)
|
||||||
|
{
|
||||||
if (toProcessIndex == 0 && patchTestAssembly == PatchTestAssembly.No)
|
if (alreadyPatched)
|
||||||
patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredTestAssembly);
|
throw new Exception("Unexpected already-patched test assembly, yet we want PatchTestAssembly.No");
|
||||||
else if (MockInjector.IsPatched(assemblyToPatch))
|
patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredTestAssembly);
|
||||||
patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.AlreadyPatched);
|
}
|
||||||
else if (assemblyPath.Contains(assemblyToPatch.Name.Name))
|
else if (alreadyPatched)
|
||||||
{
|
{
|
||||||
mockInjector.Patch(assemblyToPatch);
|
patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.AlreadyPatched);
|
||||||
|
}
|
||||||
// atomic write of file with backup
|
else
|
||||||
var tmpPath = assemblyToPatchPath.Split(new[] {".dll"}, StringSplitOptions.None)[0] +
|
{
|
||||||
".tmp";
|
patchResult = Patch(assemblyToPatchPath, assemblyToPatch);
|
||||||
File.Delete(tmpPath);
|
|
||||||
assemblyToPatch.Write(tmpPath); //$$$$, new WriterParameters { WriteSymbols = true }); // getting exception, haven't looked into it yet
|
|
||||||
assemblyToPatch.Dispose();
|
|
||||||
var originalPath = GetPatchBackupPathFor(assemblyToPatchPath);
|
|
||||||
File.Replace(tmpPath, assemblyToPatchPath, originalPath);
|
|
||||||
Verify(assemblyToPatchPath);
|
|
||||||
// $$$ TODO: move pdb file too
|
|
||||||
|
|
||||||
patchResult = new PatchResult(assemblyToPatchPath, originalPath, PatchState.Patched);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{ // TODO: Nope
|
|
||||||
patchResult = default(PatchResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
patchResults.Add(assemblyToPatchPath, patchResult);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return patchResults.Values;
|
patchResults.Add(assemblyToPatchPath, patchResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PatchResult Patch(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch)
|
||||||
|
{
|
||||||
|
mockInjector.Patch(assemblyToPatch);
|
||||||
|
|
||||||
|
// atomic write of file with backup
|
||||||
|
// TODO: skip backup if existing file already patched. want the .orig to only be the unpatched file.
|
||||||
|
|
||||||
|
// write to tmp and release the lock
|
||||||
|
var tmpPath = assemblyToPatchPath.ChangeExtension(".tmp");
|
||||||
|
tmpPath.DeleteIfExists();
|
||||||
|
assemblyToPatch.Write(tmpPath); // $$$ , new WriterParameters { WriteSymbols = true }); see https://github.com/jbevain/cecil/issues/421
|
||||||
|
assemblyToPatch.Dispose();
|
||||||
|
|
||||||
|
if ((patchOptions & PatchOptions.SkipPeVerify) == 0)
|
||||||
|
PeVerify.Verify(tmpPath);
|
||||||
|
|
||||||
|
// move the actual file to backup, and move the tmp to actual
|
||||||
|
var backupPath = GetPatchBackupPathFor(assemblyToPatchPath);
|
||||||
|
File.Replace(tmpPath, assemblyToPatchPath, backupPath);
|
||||||
|
|
||||||
|
// TODO: move pdb file too
|
||||||
|
|
||||||
|
return new PatchResult(assemblyToPatchPath, backupPath, PatchState.Patched);
|
||||||
|
}
|
||||||
|
|
||||||
|
return patchResults.Values;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix
|
static void EnsureMockTypesInFolder(NPath targetFolder)
|
||||||
const string peVerifyLocation = @"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\x64\PEVerify.exe";
|
|
||||||
static void Verify(string assemblyName)
|
|
||||||
{
|
{
|
||||||
var p = new Process
|
// ensure that our assembly with the mock types is discoverable by putting in the same folder as the dll that is having its types
|
||||||
{
|
// injected into it. we could mess with the assembly resolver to avoid this, but that won't solve the issue for appdomains and
|
||||||
StartInfo =
|
// other environments that we don't control, like peverify.
|
||||||
{
|
|
||||||
Arguments = $"/nologo \"{assemblyName}\"",
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
FileName = peVerifyLocation,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
RedirectStandardOutput = true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var error = "";
|
var mockTypesSrcPath = new NPath(MockInjector.MockTypesAssembly.Location);
|
||||||
var output = "";
|
var mockTypesDstPath = targetFolder.Combine(mockTypesSrcPath.FileName);
|
||||||
|
|
||||||
p.OutputDataReceived += (_, e) => output += $"{e.Data}\n";
|
if (mockTypesSrcPath != mockTypesDstPath)
|
||||||
p.ErrorDataReceived += (_, e) => error += $"{e.Data}\n";
|
mockTypesSrcPath.Copy(mockTypesDstPath);
|
||||||
|
|
||||||
p.Start();
|
|
||||||
p.BeginOutputReadLine();
|
|
||||||
p.BeginErrorReadLine();
|
|
||||||
|
|
||||||
p.WaitForExit();
|
|
||||||
|
|
||||||
Console.WriteLine(assemblyName);
|
|
||||||
p.ExitCode.ShouldBe(0, () => $"{error}\n{output}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyCollection<PatchResult> PatchAssemblies(
|
public static IReadOnlyCollection<PatchResult> PatchAssemblies(
|
||||||
|
@ -160,26 +155,23 @@ namespace NSubstitute.Elevated.Weaver
|
||||||
}
|
}
|
||||||
|
|
||||||
var nsubElevatedPath = Path.Combine(testAssemblyFolder, "NSubstitute.Elevated.dll");
|
var nsubElevatedPath = Path.Combine(testAssemblyFolder, "NSubstitute.Elevated.dll");
|
||||||
using (var nsubElevatedAssembly = AssemblyDefinition.ReadAssembly(nsubElevatedPath))
|
var mockInjector = new MockInjector();
|
||||||
|
|
||||||
|
foreach (var assemblyPath in testAssemblyPaths)
|
||||||
{
|
{
|
||||||
var mockInjector = new MockInjector(nsubElevatedAssembly);
|
var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath);
|
||||||
|
mockInjector.Patch(assemblyDefinition);
|
||||||
foreach (var assemblyPath in testAssemblyPaths)
|
// atomic write of file with backup
|
||||||
{
|
var tmpPath = assemblyPath.Split(new[] { ".dll" }, StringSplitOptions.None)[0] + ".tmp";
|
||||||
var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath);
|
File.Delete(tmpPath);
|
||||||
mockInjector.Patch(assemblyDefinition);
|
assemblyDefinition.Write(tmpPath);//$$$$, new WriterParameters { WriteSymbols = true }); // getting exception, haven't looked into it yet
|
||||||
// atomic write of file with backup
|
assemblyDefinition.Dispose();
|
||||||
var tmpPath = assemblyPath.Split(new[] { ".dll" }, StringSplitOptions.None)[0] + ".tmp";
|
/*var originalPath = GetPatchBackupPathFor(assemblyToPatchPath);
|
||||||
File.Delete(tmpPath);
|
File.Replace(tmpPath, assemblyToPatchPath, originalPath);*/
|
||||||
assemblyDefinition.Write(tmpPath);//$$$$, new WriterParameters { WriteSymbols = true }); // getting exception, haven't looked into it yet
|
// $$$ TODO: move pdb file too
|
||||||
assemblyDefinition.Dispose();
|
|
||||||
/*var originalPath = GetPatchBackupPathFor(assemblyToPatchPath);
|
|
||||||
File.Replace(tmpPath, assemblyToPatchPath, originalPath);*/
|
|
||||||
// $$$ TODO: move pdb file too
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,27 +20,30 @@ namespace NSubstitute.Elevated.Weaver
|
||||||
readonly TypeDefinition m_MockPlaceholderType;
|
readonly TypeDefinition m_MockPlaceholderType;
|
||||||
readonly MethodDefinition m_PatchedAssemblyBridgeTryMock;
|
readonly MethodDefinition m_PatchedAssemblyBridgeTryMock;
|
||||||
|
|
||||||
|
public static Assembly MockTypesAssembly { get; } = Assembly.GetExecutingAssembly();
|
||||||
public const string InjectedMockStaticDataName = "__mock__staticData", InjectedMockDataName = "__mock__data";
|
public const string InjectedMockStaticDataName = "__mock__staticData", InjectedMockDataName = "__mock__data";
|
||||||
|
|
||||||
static MockInjector()
|
static MockInjector()
|
||||||
{
|
{
|
||||||
k_MarkAsPatchedKey = Assembly.GetExecutingAssembly().GetName().Name;
|
var assemblyHash = MockTypesAssembly.Evidence.GetHostEvidence<Hash>();
|
||||||
|
|
||||||
var assemblyHash = Assembly.GetExecutingAssembly().Evidence.GetHostEvidence<Hash>();
|
|
||||||
if (assemblyHash == null)
|
if (assemblyHash == null)
|
||||||
throw new Exception("Assembly not stamped with a hash");
|
throw new Exception("Assembly not stamped with a hash");
|
||||||
|
|
||||||
|
k_MarkAsPatchedKey = MockTypesAssembly.GetName().Name;
|
||||||
k_MarkAsPatchedValue = assemblyHash.SHA1.ToHexString();
|
k_MarkAsPatchedValue = assemblyHash.SHA1.ToHexString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MockInjector(AssemblyDefinition nsubElevatedAssembly)
|
public MockInjector()
|
||||||
{
|
{
|
||||||
m_MockPlaceholderType = nsubElevatedAssembly.MainModule
|
using (var mockTypesDefinition = AssemblyDefinition.ReadAssembly(MockTypesAssembly.Location))
|
||||||
.GetType(typeof(MockPlaceholderType).FullName);
|
{
|
||||||
|
m_MockPlaceholderType = mockTypesDefinition.MainModule
|
||||||
|
.GetType(typeof(MockPlaceholderType).FullName);
|
||||||
|
|
||||||
m_PatchedAssemblyBridgeTryMock = nsubElevatedAssembly.MainModule
|
m_PatchedAssemblyBridgeTryMock = mockTypesDefinition.MainModule
|
||||||
.GetType(typeof(PatchedAssemblyBridge).FullName)
|
.GetType(typeof(PatchedAssemblyBridge).FullName)
|
||||||
.Methods.Single(m => m.Name == nameof(PatchedAssemblyBridge.TryMock));
|
.Methods.Single(m => m.Name == nameof(PatchedAssemblyBridge.TryMock));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Patch(AssemblyDefinition assembly)
|
public void Patch(AssemblyDefinition assembly)
|
||||||
|
@ -96,6 +99,8 @@ namespace NSubstitute.Elevated.Weaver
|
||||||
return;
|
return;
|
||||||
if (type.Name == "<Module>")
|
if (type.Name == "<Module>")
|
||||||
return;
|
return;
|
||||||
|
if (type.BaseType.FullName == "System.MulticastDelegate")
|
||||||
|
return;
|
||||||
if (type.IsExplicitLayout)
|
if (type.IsExplicitLayout)
|
||||||
return;
|
return;
|
||||||
if (type.CustomAttributes.Any(a => a.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName))
|
if (type.CustomAttributes.Any(a => a.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName))
|
||||||
|
@ -146,12 +151,9 @@ namespace NSubstitute.Elevated.Weaver
|
||||||
};
|
};
|
||||||
ctor.Parameters.Add(new ParameterDefinition(type.Module.ImportReference(m_MockPlaceholderType)));
|
ctor.Parameters.Add(new ParameterDefinition(type.Module.ImportReference(m_MockPlaceholderType)));
|
||||||
|
|
||||||
var body = ctor.Body;
|
var il = ctor.Body.GetILProcessor();
|
||||||
body.Instructions.Clear();
|
|
||||||
|
|
||||||
var il = body.GetILProcessor();
|
var baseCtors = type.BaseType.Resolve().GetConstructors().Where(c=> !c.IsStatic);
|
||||||
|
|
||||||
var baseCtors = type.BaseType.Resolve().GetConstructors();
|
|
||||||
|
|
||||||
var baseMockCtor = (MethodReference)baseCtors.SingleOrDefault(c => c.Parameters.SequenceEqual(ctor.Parameters));
|
var baseMockCtor = (MethodReference)baseCtors.SingleOrDefault(c => c.Parameters.SequenceEqual(ctor.Parameters));
|
||||||
if (baseMockCtor != null)
|
if (baseMockCtor != null)
|
||||||
|
@ -179,7 +181,7 @@ namespace NSubstitute.Elevated.Weaver
|
||||||
|
|
||||||
void Patch(MethodDefinition method, AssemblyDefinition assembly)
|
void Patch(MethodDefinition method, AssemblyDefinition assembly)
|
||||||
{
|
{
|
||||||
if (method.IsCompilerControlled || method.IsConstructor || method.IsAbstract)
|
if (method.IsCompilerControlled || method.IsConstructor || method.IsAbstract || !method.HasBody)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
method.Body.InitLocals = true;
|
method.Body.InitLocals = true;
|
||||||
|
|
|
@ -1,26 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Unity.Core;
|
using Unity.Core;
|
||||||
|
|
||||||
namespace NSubstitute.Elevated.Weaver
|
namespace NSubstitute.Elevated.Weaver
|
||||||
{
|
{
|
||||||
public class PeVerifyException : Exception
|
public class PeVerifyException : Exception
|
||||||
{
|
{
|
||||||
public PeVerifyException(string message, int exitCode, string output)
|
public PeVerifyException(string assemblyName, int exitCode, IEnumerable<string> log)
|
||||||
: base(message)
|
: base(
|
||||||
{
|
$"Failed to PEVerify assembly '{assemblyName}' (exit={exitCode})\n\n"
|
||||||
ExitCode = exitCode;
|
+ log.StringJoin('\n')) { }
|
||||||
Output = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int ExitCode { get; }
|
|
||||||
public string Output { get; }
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{Message} (exit={ExitCode})\n{Output}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PeVerify
|
public static class PeVerify
|
||||||
|
@ -34,15 +23,13 @@ namespace NSubstitute.Elevated.Weaver
|
||||||
|
|
||||||
public static void Verify(string assemblyName)
|
public static void Verify(string assemblyName)
|
||||||
{
|
{
|
||||||
var stdout = new List<string>();
|
var log = new List<string>();
|
||||||
var stderr = new List<string>();
|
var rc = ProcessUtility.ExecuteCommandLine(ExePath, new[] { "/nologo", assemblyName }, null, log, log);
|
||||||
|
|
||||||
var rc = ProcessUtility.ExecuteCommandLine(ExePath, new[] { "/nologo", assemblyName }, null, stdout, stderr);
|
|
||||||
|
|
||||||
// TODO: not great to just throw like this vs. returning an error structure
|
// TODO: not great to just throw like this vs. returning an error structure
|
||||||
// TODO: will it return 0 even if there are warnings?
|
// TODO: will it return 0 even if there are warnings?
|
||||||
if (rc != 0)
|
if (rc != 0)
|
||||||
throw new PeVerifyException($"Failed to PEVerify assembly '{assemblyName}'", rc, stderr.Concat(stdout).StringJoin('\n'));
|
throw new PeVerifyException(assemblyName, rc, log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using SystemUnderTest;
|
using SystemUnderTest;
|
||||||
|
using NiceIO;
|
||||||
using NSubstitute.Exceptions;
|
using NSubstitute.Exceptions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
|
|
||||||
namespace NSubstitute.Elevated.Tests
|
namespace NSubstitute.Elevated.Tests
|
||||||
{
|
{
|
||||||
[TestFixture]
|
|
||||||
class BasicTests
|
class BasicTests
|
||||||
{
|
{
|
||||||
IDisposable m_Dispose;
|
IDisposable m_Dispose;
|
||||||
|
@ -14,7 +14,10 @@ namespace NSubstitute.Elevated.Tests
|
||||||
[OneTimeSetUp]
|
[OneTimeSetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
m_Dispose = ElevatedSubstitutionContext.AutoHook(typeof(BasicTests).Assembly.Location, new [] {"SystemUnderTest"});
|
var buildFolder = new NPath(GetType().Assembly.Location).Parent;
|
||||||
|
var systemUnderTest = buildFolder.Combine("SystemUnderTest.dll"); // do not access type directly, want to avoid loading the assembly until it's patched
|
||||||
|
|
||||||
|
m_Dispose = ElevatedSubstitutionContext.AutoHook(systemUnderTest);
|
||||||
}
|
}
|
||||||
|
|
||||||
[OneTimeTearDown]
|
[OneTimeTearDown]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Mono.Cecil;
|
||||||
using Mono.Cecil.Rocks;
|
using Mono.Cecil.Rocks;
|
||||||
using NSubstitute.Elevated.Weaver;
|
using NSubstitute.Elevated.Weaver;
|
||||||
using NSubstitute.Elevated.WeaverInternals;
|
using NSubstitute.Elevated.WeaverInternals;
|
||||||
|
@ -8,10 +9,32 @@ using Shouldly;
|
||||||
|
|
||||||
namespace NSubstitute.Elevated.Tests.Utilities
|
namespace NSubstitute.Elevated.Tests.Utilities
|
||||||
{
|
{
|
||||||
[TestFixture]
|
public class DependentAssemblyTests : PatchingFixture
|
||||||
public class ElevatedWeaverTests
|
|
||||||
{
|
{
|
||||||
TestAssembly m_FixtureTestAssembly;
|
[Test]
|
||||||
|
public void PatchingDependentAssembly_WhenAlreadyLoaded_ShouldThrow()
|
||||||
|
{/*
|
||||||
|
var dependentDllName = GetType().Name + "_dependent";
|
||||||
|
var dependentAssemblyPath = Compile(BaseDir, dependentDllName, "public class ReferencedType { }" );
|
||||||
|
var usingAssemblyPath = Compile(BaseDir, GetType().Name + "_using", "public class UsingType : ReferencedType { }", dependentDllName);
|
||||||
|
|
||||||
|
var dependentAssembly = PatchAndValidateAllDependentAssemblies(dependentAssemblyPath);
|
||||||
|
var type = GetType(dependentAssembly, "ReferencedType");
|
||||||
|
MockInjector.IsPatched(type).ShouldBeFalse();*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: tests to add
|
||||||
|
//
|
||||||
|
// sample: CombinatorialAttribute : CombiningStrategyAttribute << this crashes
|
||||||
|
// class Base { Base(int) { } }
|
||||||
|
// class Derived : Base { Derived() : base(1) { } }
|
||||||
|
//
|
||||||
|
// class with `public delegate void Blah();`
|
||||||
|
|
||||||
|
public class ElevatedWeaverTests : PatchingFixture
|
||||||
|
{
|
||||||
|
AssemblyDefinition m_TestAssembly;
|
||||||
|
|
||||||
const string k_FixtureTestCode = @"
|
const string k_FixtureTestCode = @"
|
||||||
|
|
||||||
|
@ -51,40 +74,48 @@ namespace NSubstitute.Elevated.Tests.Utilities
|
||||||
[OneTimeSetUp]
|
[OneTimeSetUp]
|
||||||
public void OneTimeSetUp()
|
public void OneTimeSetUp()
|
||||||
{
|
{
|
||||||
m_FixtureTestAssembly = new TestAssembly(nameof(ElevatedWeaverTests), k_FixtureTestCode);
|
var testAssemblyPath = Compile(GetType().Name, k_FixtureTestCode);
|
||||||
|
|
||||||
|
var results = ElevatedWeaver.PatchAllDependentAssemblies(testAssemblyPath, PatchOptions.PatchTestAssembly);
|
||||||
|
results.Count.ShouldBe(2);
|
||||||
|
results.ShouldContain(new PatchResult("mscorlib", null, PatchState.IgnoredOutsideAllowedPaths));
|
||||||
|
results.ShouldContain(new PatchResult(testAssemblyPath, ElevatedWeaver.GetPatchBackupPathFor(testAssemblyPath), PatchState.Patched));
|
||||||
|
|
||||||
|
m_TestAssembly = AssemblyDefinition.ReadAssembly(testAssemblyPath);
|
||||||
|
MockInjector.IsPatched(m_TestAssembly).ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[OneTimeTearDown]
|
[OneTimeTearDown]
|
||||||
public void OneTimeTearDown()
|
public void OneTimeTearDown()
|
||||||
{
|
{
|
||||||
m_FixtureTestAssembly?.Dispose();
|
m_TestAssembly?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Interfaces_ShouldNotPatch()
|
public void Interfaces_ShouldNotPatch()
|
||||||
{
|
{
|
||||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.Interface");
|
var type = GetType(m_TestAssembly, "ShouldNotPatch.Interface");
|
||||||
MockInjector.IsPatched(type).ShouldBeFalse();
|
MockInjector.IsPatched(type).ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void PotentiallyBlittableStructs_ShouldNotPatch()
|
public void PotentiallyBlittableStructs_ShouldNotPatch()
|
||||||
{
|
{
|
||||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.StructWithLayoutAttr");
|
var type = GetType(m_TestAssembly, "ShouldNotPatch.StructWithLayoutAttr");
|
||||||
MockInjector.IsPatched(type).ShouldBeFalse();
|
MockInjector.IsPatched(type).ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void PrivateNestedTypes_ShouldNotPatch()
|
public void PrivateNestedTypes_ShouldNotPatch()
|
||||||
{
|
{
|
||||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.ClassWithPrivateNestedType/PrivateNested");
|
var type = GetType(m_TestAssembly, "ShouldNotPatch.ClassWithPrivateNestedType/PrivateNested");
|
||||||
MockInjector.IsPatched(type).ShouldBeFalse();
|
MockInjector.IsPatched(type).ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void GeneratedTypes_ShouldNotPatch()
|
public void GeneratedTypes_ShouldNotPatch()
|
||||||
{
|
{
|
||||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.ClassWithGeneratedNestedType");
|
var type = GetType(m_TestAssembly, "ShouldNotPatch.ClassWithGeneratedNestedType");
|
||||||
type.NestedTypes.Count.ShouldBe(1); // this is the yield state machine, will be mangled name
|
type.NestedTypes.Count.ShouldBe(1); // this is the yield state machine, will be mangled name
|
||||||
MockInjector.IsPatched(type.NestedTypes[0]).ShouldBeFalse();
|
MockInjector.IsPatched(type.NestedTypes[0]).ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
@ -92,21 +123,21 @@ namespace NSubstitute.Elevated.Tests.Utilities
|
||||||
[Test]
|
[Test]
|
||||||
public void TopLevelClass_ShouldPatch()
|
public void TopLevelClass_ShouldPatch()
|
||||||
{
|
{
|
||||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes");
|
var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes");
|
||||||
MockInjector.IsPatched(type).ShouldBeTrue();
|
MockInjector.IsPatched(type).ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void PublicNestedClasses_ShouldPatch()
|
public void PublicNestedClasses_ShouldPatch()
|
||||||
{
|
{
|
||||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes/PublicNested");
|
var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes/PublicNested");
|
||||||
MockInjector.IsPatched(type).ShouldBeTrue();
|
MockInjector.IsPatched(type).ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void InternalNestedClasses_ShouldPatch()
|
public void InternalNestedClasses_ShouldPatch()
|
||||||
{
|
{
|
||||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes/InternalNested");
|
var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes/InternalNested");
|
||||||
MockInjector.IsPatched(type).ShouldBeTrue();
|
MockInjector.IsPatched(type).ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +145,7 @@ namespace NSubstitute.Elevated.Tests.Utilities
|
||||||
public void Injection_IsConsistentForAllTypes()
|
public void Injection_IsConsistentForAllTypes()
|
||||||
{
|
{
|
||||||
// whatever the reasons are for a given type getting patched or not, we want it to be internally consistent
|
// whatever the reasons are for a given type getting patched or not, we want it to be internally consistent
|
||||||
foreach (var type in m_FixtureTestAssembly.SelectTypes(IncludeNested.Yes))
|
foreach (var type in SelectTypes(m_TestAssembly, IncludeNested.Yes))
|
||||||
{
|
{
|
||||||
var mockStaticField = type.Fields.SingleOrDefault(f => f.Name == MockInjector.InjectedMockStaticDataName);
|
var mockStaticField = type.Fields.SingleOrDefault(f => f.Name == MockInjector.InjectedMockStaticDataName);
|
||||||
var mockField = type.Fields.SingleOrDefault(f => f.Name == MockInjector.InjectedMockDataName);
|
var mockField = type.Fields.SingleOrDefault(f => f.Name == MockInjector.InjectedMockDataName);
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System;
|
||||||
|
using System.CodeDom.Compiler;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Mono.Cecil;
|
||||||
|
using NiceIO;
|
||||||
|
using NSubstitute.Elevated.Weaver;
|
||||||
|
using Shouldly;
|
||||||
|
using Unity.Core;
|
||||||
|
|
||||||
|
namespace NSubstitute.Elevated.Tests.Utilities
|
||||||
|
{
|
||||||
|
public abstract class PatchingFixture : TestFileSystemFixture
|
||||||
|
{
|
||||||
|
public NPath Compile(string testAssemblyName, string sourceCode, params string[] dependentAssemblyNames)
|
||||||
|
{
|
||||||
|
var testAssemblyPath = BaseDir.Combine(testAssemblyName + ".dll");
|
||||||
|
|
||||||
|
// set up to compile
|
||||||
|
|
||||||
|
var compiler = new Microsoft.CSharp.CSharpCodeProvider();
|
||||||
|
var compilerArgs = new CompilerParameters
|
||||||
|
{
|
||||||
|
OutputAssembly = testAssemblyPath,
|
||||||
|
IncludeDebugInformation = true,
|
||||||
|
CompilerOptions = "/o- /debug+ /warn:0"
|
||||||
|
};
|
||||||
|
compilerArgs.ReferencedAssemblies.Add(typeof(int).Assembly.Location); // mscorlib
|
||||||
|
compilerArgs.ReferencedAssemblies.AddRange(
|
||||||
|
dependentAssemblyNames.Select(n => BaseDir.Combine(n + ".dll").ToString()));
|
||||||
|
|
||||||
|
// compile and handle errors
|
||||||
|
|
||||||
|
var compilerResult = compiler.CompileAssemblyFromSource(compilerArgs, sourceCode);
|
||||||
|
if (compilerResult.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
var errorText = compilerResult.Errors
|
||||||
|
.OfType<CompilerError>()
|
||||||
|
.Select(e => $"({e.Line},{e.Column}): error {e.ErrorNumber}: {e.ErrorText}")
|
||||||
|
.Prepend("Compiler errors:")
|
||||||
|
.StringJoin("\n");
|
||||||
|
throw new Exception(errorText);
|
||||||
|
}
|
||||||
|
|
||||||
|
testAssemblyPath.ShouldBe(new NPath(compilerResult.PathToAssembly));
|
||||||
|
|
||||||
|
PeVerify.Verify(testAssemblyPath); // sanity check on what the compiler generated
|
||||||
|
|
||||||
|
return testAssemblyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TypeDefinition GetType(AssemblyDefinition testAssembly, string typeName)
|
||||||
|
=> testAssembly.MainModule.GetType(typeName);
|
||||||
|
public IEnumerable<TypeDefinition> SelectTypes(AssemblyDefinition testAssembly, IncludeNested includeNested)
|
||||||
|
=> testAssembly.SelectTypes(includeNested);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,74 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.CodeDom.Compiler;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using Mono.Cecil;
|
|
||||||
using NSubstitute.Elevated.Weaver;
|
|
||||||
using Shouldly;
|
|
||||||
using Unity.Core;
|
|
||||||
|
|
||||||
namespace NSubstitute.Elevated.Tests.Utilities
|
|
||||||
{
|
|
||||||
public class TestAssembly : IDisposable
|
|
||||||
{
|
|
||||||
string m_TestAssemblyPath;
|
|
||||||
AssemblyDefinition m_TestAssembly;
|
|
||||||
|
|
||||||
public TestAssembly(string assemblyName, string testSourceCodeFile)
|
|
||||||
{
|
|
||||||
var outputFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
|
||||||
outputFolder.ShouldNotBeNull();
|
|
||||||
|
|
||||||
m_TestAssemblyPath = Path.Combine(outputFolder, assemblyName + ".dll");
|
|
||||||
|
|
||||||
var compiler = new Microsoft.CSharp.CSharpCodeProvider();
|
|
||||||
var compilerArgs = new CompilerParameters
|
|
||||||
{
|
|
||||||
OutputAssembly = m_TestAssemblyPath,
|
|
||||||
IncludeDebugInformation = true,
|
|
||||||
CompilerOptions = "/o- /debug+ /warn:0"
|
|
||||||
};
|
|
||||||
compilerArgs.ReferencedAssemblies.Add(typeof(Enumerable).Assembly.Location);
|
|
||||||
|
|
||||||
var compilerResult = compiler.CompileAssemblyFromSource(compilerArgs, testSourceCodeFile);
|
|
||||||
if (compilerResult.Errors.Count > 0)
|
|
||||||
{
|
|
||||||
var errorText = compilerResult.Errors
|
|
||||||
.OfType<CompilerError>()
|
|
||||||
.Select(e => $"({e.Line},{e.Column}): error {e.ErrorNumber}: {e.ErrorText}")
|
|
||||||
.Prepend("Compiler errors:")
|
|
||||||
.StringJoin("\n");
|
|
||||||
throw new Exception(errorText);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_TestAssemblyPath = compilerResult.PathToAssembly;
|
|
||||||
|
|
||||||
PeVerify.Verify(m_TestAssemblyPath); // pre-check..sometimes we can compile code that doesn't verify
|
|
||||||
|
|
||||||
var results = ElevatedWeaver.PatchAllDependentAssemblies(m_TestAssemblyPath, PatchTestAssembly.Yes, new [] { new FileInfo(m_TestAssemblyPath).Name.Replace(".dll", string.Empty) });
|
|
||||||
results.Count.ShouldBe(2);
|
|
||||||
results.ShouldContain(new PatchResult("mscorlib", null, PatchState.IgnoredOutsideAllowedPaths));
|
|
||||||
results.ShouldContain(new PatchResult(m_TestAssemblyPath, ElevatedWeaver.GetPatchBackupPathFor(m_TestAssemblyPath), PatchState.Patched));
|
|
||||||
|
|
||||||
m_TestAssembly = AssemblyDefinition.ReadAssembly(m_TestAssemblyPath);
|
|
||||||
MockInjector.IsPatched(m_TestAssembly).ShouldBeTrue();
|
|
||||||
|
|
||||||
PeVerify.Verify(m_TestAssemblyPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
m_TestAssembly.Dispose();
|
|
||||||
|
|
||||||
var dir = new DirectoryInfo(Path.GetDirectoryName(m_TestAssemblyPath));
|
|
||||||
foreach (var file in dir.EnumerateFiles(Path.GetFileNameWithoutExtension(m_TestAssemblyPath) + ".*"))
|
|
||||||
File.Delete(file.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TypeDefinition GetType(string typeName) => m_TestAssembly.MainModule.GetType(typeName);
|
|
||||||
public IEnumerable<TypeDefinition> SelectTypes(IncludeNested includeNested) => m_TestAssembly.SelectTypes(includeNested);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
using System;
|
||||||
|
using NiceIO;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace NSubstitute.Elevated.Tests.Utilities
|
||||||
|
{
|
||||||
|
public abstract class TestFileSystemFixture
|
||||||
|
{
|
||||||
|
protected NPath BaseDir { private set; get; }
|
||||||
|
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public void InitFixture()
|
||||||
|
{
|
||||||
|
var testDir = new NPath(TestContext.CurrentContext.TestDirectory);
|
||||||
|
BaseDir = testDir.Combine("testfs_" + GetType().Name);
|
||||||
|
CreateTestFileSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
[OneTimeTearDown]
|
||||||
|
public void TearDownFixture() => DeleteTestFileSystem();
|
||||||
|
|
||||||
|
protected void CreateTestFileSystem()
|
||||||
|
{
|
||||||
|
DeleteTestFileSystem();
|
||||||
|
BaseDir.CreateDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DeleteTestFileSystem() => BaseDir.DeleteIfExists();
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче