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:
Scott Bilas 2018-11-07 17:22:47 +01:00
Родитель bd47b10bc6
Коммит 1d3cd92503
9 изменённых файлов: 274 добавлений и 243 удалений

Просмотреть файл

@ -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();
}
}