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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using NiceIO;
|
||||
using NSubstitute.Core;
|
||||
using NSubstitute.Core.Arguments;
|
||||
using NSubstitute.Elevated.Weaver;
|
||||
|
@ -31,13 +31,16 @@ namespace NSubstitute.Elevated
|
|||
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 thisContext = new ElevatedSubstitutionContext(hookedContext);
|
||||
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(() =>
|
||||
{
|
||||
|
|
|
@ -6,12 +6,17 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using Mono.Cecil;
|
||||
using Shouldly;
|
||||
using NiceIO;
|
||||
using Unity.Core;
|
||||
|
||||
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
|
||||
{
|
||||
|
@ -20,122 +25,112 @@ namespace NSubstitute.Elevated.Weaver
|
|||
public static string GetPatchBackupPathFor(string path)
|
||||
=> path + k_PatchBackupExtension;
|
||||
|
||||
public static IReadOnlyCollection<PatchResult> PatchAllDependentAssemblies([NotNull] string testAssemblyPath,
|
||||
PatchTestAssembly patchTestAssembly = PatchTestAssembly.No, IEnumerable<string> assemblyPath = null) // typically we don't want to patch the test assembly itself, only the systems under test
|
||||
public static IReadOnlyCollection<PatchResult> PatchAllDependentAssemblies(NPath testAssemblyPath, PatchOptions patchOptions)
|
||||
{
|
||||
var testAssemblyFolder = Path.GetDirectoryName(testAssemblyPath);
|
||||
if (testAssemblyFolder.IsNullOrEmpty())
|
||||
throw new Exception("Unable to find folder for test assembly");
|
||||
testAssemblyFolder = Path.GetFullPath(testAssemblyFolder);
|
||||
// TODO: ensure we do not have any assemblies that we want to patch already loaded
|
||||
// (this will require the separate in-memory patching ability)
|
||||
|
||||
// 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);
|
||||
if (thisAssemblyFolder.IsNullOrEmpty())
|
||||
throw new Exception("Can only patch assemblies on disk");
|
||||
thisAssemblyFolder = Path.GetFullPath(thisAssemblyFolder);
|
||||
var assemblyToPatchPath = toProcess[toProcessIndex];
|
||||
|
||||
// keep things really simple, at least for now
|
||||
if (string.Compare(testAssemblyFolder, thisAssemblyFolder, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
throw new Exception("All assemblies must be in the same folder");
|
||||
// as we accumulate dependencies recursively, we will probably get some duplicates we can early-out on
|
||||
if (patchResults.ContainsKey(assemblyToPatchPath))
|
||||
continue;
|
||||
|
||||
using (var assemblyToPatch = AssemblyDefinition.ReadAssembly(assemblyToPatchPath))
|
||||
{
|
||||
GatherReferences(assemblyToPatchPath, assemblyToPatch);
|
||||
TryPatch(assemblyToPatchPath, assemblyToPatch);
|
||||
}
|
||||
}
|
||||
|
||||
var nsubElevatedPath = Path.Combine(testAssemblyFolder, "NSubstitute.Elevated.dll");
|
||||
using (var nsubElevatedAssembly = AssemblyDefinition.ReadAssembly(nsubElevatedPath))
|
||||
void GatherReferences(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch)
|
||||
{
|
||||
var mockInjector = new MockInjector(nsubElevatedAssembly);
|
||||
var toProcess = new List<string> { Path.GetFullPath(testAssemblyPath) };
|
||||
var patchResults = new Dictionary<string, PatchResult>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var toProcessIndex = 0; toProcessIndex < toProcess.Count; ++toProcessIndex)
|
||||
foreach (var referencedAssembly in assemblyToPatch.Modules.SelectMany(m => m.AssemblyReferences))
|
||||
{
|
||||
var assemblyToPatchPath = toProcess[toProcessIndex];
|
||||
if (patchResults.ContainsKey(assemblyToPatchPath))
|
||||
continue;
|
||||
// only patch dll's we "own", that are in the same folder as the test assembly
|
||||
var referencedAssemblyPath = assemblyToPatchPath.Parent.Combine(referencedAssembly.Name + ".dll");
|
||||
|
||||
if (!Path.IsPathRooted(assemblyToPatchPath))
|
||||
throw new Exception($"Unexpected non-rooted assembly path '{assemblyToPatchPath}'");
|
||||
if (referencedAssemblyPath.FileExists())
|
||||
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))
|
||||
{
|
||||
foreach (var referencedAssembly in assemblyToPatch.Modules.SelectMany(m => m.AssemblyReferences))
|
||||
{
|
||||
// only patch dll's we "own", that are in the same folder as the test assembly
|
||||
var foundPath = Path.Combine(testAssemblyFolder, referencedAssembly.Name + ".dll");
|
||||
void TryPatch(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch)
|
||||
{
|
||||
PatchResult patchResult;
|
||||
|
||||
if (File.Exists(foundPath))
|
||||
toProcess.Add(foundPath);
|
||||
else if (!patchResults.ContainsKey(referencedAssembly.Name))
|
||||
patchResults.Add(referencedAssembly.Name, new PatchResult(referencedAssembly.Name, null, PatchState.IgnoredOutsideAllowedPaths));
|
||||
}
|
||||
var alreadyPatched = MockInjector.IsPatched(assemblyToPatch);
|
||||
|
||||
PatchResult patchResult;
|
||||
|
||||
if (toProcessIndex == 0 && patchTestAssembly == PatchTestAssembly.No)
|
||||
patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredTestAssembly);
|
||||
else if (MockInjector.IsPatched(assemblyToPatch))
|
||||
patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.AlreadyPatched);
|
||||
else if (assemblyPath.Contains(assemblyToPatch.Name.Name))
|
||||
{
|
||||
mockInjector.Patch(assemblyToPatch);
|
||||
|
||||
// atomic write of file with backup
|
||||
var tmpPath = assemblyToPatchPath.Split(new[] {".dll"}, StringSplitOptions.None)[0] +
|
||||
".tmp";
|
||||
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);
|
||||
}
|
||||
if (assemblyToPatchPath == testAssemblyPath && (patchOptions & PatchOptions.PatchTestAssembly) == 0)
|
||||
{
|
||||
if (alreadyPatched)
|
||||
throw new Exception("Unexpected already-patched test assembly, yet we want PatchTestAssembly.No");
|
||||
patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredTestAssembly);
|
||||
}
|
||||
else if (alreadyPatched)
|
||||
{
|
||||
patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.AlreadyPatched);
|
||||
}
|
||||
else
|
||||
{
|
||||
patchResult = Patch(assemblyToPatchPath, assemblyToPatch);
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
static void EnsureMockTypesInFolder(NPath targetFolder)
|
||||
{
|
||||
var p = new Process
|
||||
{
|
||||
StartInfo =
|
||||
{
|
||||
Arguments = $"/nologo \"{assemblyName}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
FileName = peVerifyLocation,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true
|
||||
}
|
||||
};
|
||||
// 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
|
||||
// other environments that we don't control, like peverify.
|
||||
|
||||
var error = "";
|
||||
var output = "";
|
||||
var mockTypesSrcPath = new NPath(MockInjector.MockTypesAssembly.Location);
|
||||
var mockTypesDstPath = targetFolder.Combine(mockTypesSrcPath.FileName);
|
||||
|
||||
p.OutputDataReceived += (_, e) => output += $"{e.Data}\n";
|
||||
p.ErrorDataReceived += (_, e) => error += $"{e.Data}\n";
|
||||
|
||||
p.Start();
|
||||
p.BeginOutputReadLine();
|
||||
p.BeginErrorReadLine();
|
||||
|
||||
p.WaitForExit();
|
||||
|
||||
Console.WriteLine(assemblyName);
|
||||
p.ExitCode.ShouldBe(0, () => $"{error}\n{output}");
|
||||
if (mockTypesSrcPath != mockTypesDstPath)
|
||||
mockTypesSrcPath.Copy(mockTypesDstPath);
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<PatchResult> PatchAssemblies(
|
||||
|
@ -160,26 +155,23 @@ namespace NSubstitute.Elevated.Weaver
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
foreach (var assemblyPath in testAssemblyPaths)
|
||||
{
|
||||
var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath);
|
||||
mockInjector.Patch(assemblyDefinition);
|
||||
// atomic write of file with backup
|
||||
var tmpPath = assemblyPath.Split(new[] { ".dll" }, StringSplitOptions.None)[0] + ".tmp";
|
||||
File.Delete(tmpPath);
|
||||
assemblyDefinition.Write(tmpPath);//$$$$, new WriterParameters { WriteSymbols = true }); // getting exception, haven't looked into it yet
|
||||
assemblyDefinition.Dispose();
|
||||
/*var originalPath = GetPatchBackupPathFor(assemblyToPatchPath);
|
||||
File.Replace(tmpPath, assemblyToPatchPath, originalPath);*/
|
||||
// $$$ TODO: move pdb file too
|
||||
}
|
||||
|
||||
return null;
|
||||
var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath);
|
||||
mockInjector.Patch(assemblyDefinition);
|
||||
// atomic write of file with backup
|
||||
var tmpPath = assemblyPath.Split(new[] { ".dll" }, StringSplitOptions.None)[0] + ".tmp";
|
||||
File.Delete(tmpPath);
|
||||
assemblyDefinition.Write(tmpPath);//$$$$, new WriterParameters { WriteSymbols = true }); // getting exception, haven't looked into it yet
|
||||
assemblyDefinition.Dispose();
|
||||
/*var originalPath = GetPatchBackupPathFor(assemblyToPatchPath);
|
||||
File.Replace(tmpPath, assemblyToPatchPath, originalPath);*/
|
||||
// $$$ TODO: move pdb file too
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,27 +20,30 @@ namespace NSubstitute.Elevated.Weaver
|
|||
readonly TypeDefinition m_MockPlaceholderType;
|
||||
readonly MethodDefinition m_PatchedAssemblyBridgeTryMock;
|
||||
|
||||
public static Assembly MockTypesAssembly { get; } = Assembly.GetExecutingAssembly();
|
||||
public const string InjectedMockStaticDataName = "__mock__staticData", InjectedMockDataName = "__mock__data";
|
||||
|
||||
static MockInjector()
|
||||
{
|
||||
k_MarkAsPatchedKey = Assembly.GetExecutingAssembly().GetName().Name;
|
||||
|
||||
var assemblyHash = Assembly.GetExecutingAssembly().Evidence.GetHostEvidence<Hash>();
|
||||
var assemblyHash = MockTypesAssembly.Evidence.GetHostEvidence<Hash>();
|
||||
if (assemblyHash == null)
|
||||
throw new Exception("Assembly not stamped with a hash");
|
||||
|
||||
k_MarkAsPatchedKey = MockTypesAssembly.GetName().Name;
|
||||
k_MarkAsPatchedValue = assemblyHash.SHA1.ToHexString();
|
||||
}
|
||||
|
||||
public MockInjector(AssemblyDefinition nsubElevatedAssembly)
|
||||
public MockInjector()
|
||||
{
|
||||
m_MockPlaceholderType = nsubElevatedAssembly.MainModule
|
||||
.GetType(typeof(MockPlaceholderType).FullName);
|
||||
using (var mockTypesDefinition = AssemblyDefinition.ReadAssembly(MockTypesAssembly.Location))
|
||||
{
|
||||
m_MockPlaceholderType = mockTypesDefinition.MainModule
|
||||
.GetType(typeof(MockPlaceholderType).FullName);
|
||||
|
||||
m_PatchedAssemblyBridgeTryMock = nsubElevatedAssembly.MainModule
|
||||
.GetType(typeof(PatchedAssemblyBridge).FullName)
|
||||
.Methods.Single(m => m.Name == nameof(PatchedAssemblyBridge.TryMock));
|
||||
m_PatchedAssemblyBridgeTryMock = mockTypesDefinition.MainModule
|
||||
.GetType(typeof(PatchedAssemblyBridge).FullName)
|
||||
.Methods.Single(m => m.Name == nameof(PatchedAssemblyBridge.TryMock));
|
||||
}
|
||||
}
|
||||
|
||||
public void Patch(AssemblyDefinition assembly)
|
||||
|
@ -96,6 +99,8 @@ namespace NSubstitute.Elevated.Weaver
|
|||
return;
|
||||
if (type.Name == "<Module>")
|
||||
return;
|
||||
if (type.BaseType.FullName == "System.MulticastDelegate")
|
||||
return;
|
||||
if (type.IsExplicitLayout)
|
||||
return;
|
||||
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)));
|
||||
|
||||
var body = ctor.Body;
|
||||
body.Instructions.Clear();
|
||||
var il = ctor.Body.GetILProcessor();
|
||||
|
||||
var il = body.GetILProcessor();
|
||||
|
||||
var baseCtors = type.BaseType.Resolve().GetConstructors();
|
||||
var baseCtors = type.BaseType.Resolve().GetConstructors().Where(c=> !c.IsStatic);
|
||||
|
||||
var baseMockCtor = (MethodReference)baseCtors.SingleOrDefault(c => c.Parameters.SequenceEqual(ctor.Parameters));
|
||||
if (baseMockCtor != null)
|
||||
|
@ -179,7 +181,7 @@ namespace NSubstitute.Elevated.Weaver
|
|||
|
||||
void Patch(MethodDefinition method, AssemblyDefinition assembly)
|
||||
{
|
||||
if (method.IsCompilerControlled || method.IsConstructor || method.IsAbstract)
|
||||
if (method.IsCompilerControlled || method.IsConstructor || method.IsAbstract || !method.HasBody)
|
||||
return;
|
||||
|
||||
method.Body.InitLocals = true;
|
||||
|
|
|
@ -1,26 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Unity.Core;
|
||||
|
||||
namespace NSubstitute.Elevated.Weaver
|
||||
{
|
||||
public class PeVerifyException : Exception
|
||||
{
|
||||
public PeVerifyException(string message, int exitCode, string output)
|
||||
: base(message)
|
||||
{
|
||||
ExitCode = exitCode;
|
||||
Output = output;
|
||||
}
|
||||
|
||||
public int ExitCode { get; }
|
||||
public string Output { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Message} (exit={ExitCode})\n{Output}";
|
||||
}
|
||||
public PeVerifyException(string assemblyName, int exitCode, IEnumerable<string> log)
|
||||
: base(
|
||||
$"Failed to PEVerify assembly '{assemblyName}' (exit={exitCode})\n\n"
|
||||
+ log.StringJoin('\n')) { }
|
||||
}
|
||||
|
||||
public static class PeVerify
|
||||
|
@ -34,15 +23,13 @@ namespace NSubstitute.Elevated.Weaver
|
|||
|
||||
public static void Verify(string assemblyName)
|
||||
{
|
||||
var stdout = new List<string>();
|
||||
var stderr = new List<string>();
|
||||
|
||||
var rc = ProcessUtility.ExecuteCommandLine(ExePath, new[] { "/nologo", assemblyName }, null, stdout, stderr);
|
||||
var log = new List<string>();
|
||||
var rc = ProcessUtility.ExecuteCommandLine(ExePath, new[] { "/nologo", assemblyName }, null, log, log);
|
||||
|
||||
// TODO: not great to just throw like this vs. returning an error structure
|
||||
// TODO: will it return 0 even if there are warnings?
|
||||
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 SystemUnderTest;
|
||||
using NiceIO;
|
||||
using NSubstitute.Exceptions;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace NSubstitute.Elevated.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
class BasicTests
|
||||
{
|
||||
IDisposable m_Dispose;
|
||||
|
@ -14,7 +14,10 @@ namespace NSubstitute.Elevated.Tests
|
|||
[OneTimeSetUp]
|
||||
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]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Rocks;
|
||||
using NSubstitute.Elevated.Weaver;
|
||||
using NSubstitute.Elevated.WeaverInternals;
|
||||
|
@ -8,10 +9,32 @@ using Shouldly;
|
|||
|
||||
namespace NSubstitute.Elevated.Tests.Utilities
|
||||
{
|
||||
[TestFixture]
|
||||
public class ElevatedWeaverTests
|
||||
public class DependentAssemblyTests : PatchingFixture
|
||||
{
|
||||
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 = @"
|
||||
|
||||
|
@ -51,40 +74,48 @@ namespace NSubstitute.Elevated.Tests.Utilities
|
|||
[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]
|
||||
public void OneTimeTearDown()
|
||||
{
|
||||
m_FixtureTestAssembly?.Dispose();
|
||||
m_TestAssembly?.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Interfaces_ShouldNotPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.Interface");
|
||||
var type = GetType(m_TestAssembly, "ShouldNotPatch.Interface");
|
||||
MockInjector.IsPatched(type).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PotentiallyBlittableStructs_ShouldNotPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.StructWithLayoutAttr");
|
||||
var type = GetType(m_TestAssembly, "ShouldNotPatch.StructWithLayoutAttr");
|
||||
MockInjector.IsPatched(type).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PrivateNestedTypes_ShouldNotPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.ClassWithPrivateNestedType/PrivateNested");
|
||||
var type = GetType(m_TestAssembly, "ShouldNotPatch.ClassWithPrivateNestedType/PrivateNested");
|
||||
MockInjector.IsPatched(type).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
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
|
||||
MockInjector.IsPatched(type.NestedTypes[0]).ShouldBeFalse();
|
||||
}
|
||||
|
@ -92,21 +123,21 @@ namespace NSubstitute.Elevated.Tests.Utilities
|
|||
[Test]
|
||||
public void TopLevelClass_ShouldPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes");
|
||||
var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes");
|
||||
MockInjector.IsPatched(type).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PublicNestedClasses_ShouldPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes/PublicNested");
|
||||
var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes/PublicNested");
|
||||
MockInjector.IsPatched(type).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InternalNestedClasses_ShouldPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes/InternalNested");
|
||||
var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes/InternalNested");
|
||||
MockInjector.IsPatched(type).ShouldBeTrue();
|
||||
}
|
||||
|
||||
|
@ -114,7 +145,7 @@ namespace NSubstitute.Elevated.Tests.Utilities
|
|||
public void Injection_IsConsistentForAllTypes()
|
||||
{
|
||||
// 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 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();
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче