Loads of iteration on tests, patcher, Core inclusion..
This commit is contained in:
Родитель
ba1b3d8dd1
Коммит
7ca0a146b9
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27009.1
|
||||
VisualStudioVersion = 15.0.27019.1
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSubstitute.Elevated", "NSubstitute.Elevated\NSubstitute.Elevated.csproj", "{771C49B1-4768-45FA-97BA-37B56268C534}"
|
||||
EndProject
|
||||
|
@ -11,6 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSubstitute.Elevated.Tests"
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36AEE027-40B5-4ECF-8A1B-4FCAE63C73B3}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
common.targets = common.targets
|
||||
..\README.md = ..\README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
@ -20,6 +21,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemUnderTest", "..\tests
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependentAssembly", "..\tests\Support\DependentAssembly\DependentAssembly.csproj", "{5F9C587F-9F8A-40E5-87CA-62C55481851C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Core", "Unity.Core\Unity.Core.csproj", "{4483F618-9ADB-4A0B-A0D4-37EDB2593F06}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Core.Tests", "Unity.Core.Tests\Unity.Core.Tests.csproj", "{6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -42,6 +47,14 @@ Global
|
|||
{5F9C587F-9F8A-40E5-87CA-62C55481851C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F9C587F-9F8A-40E5-87CA-62C55481851C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F9C587F-9F8A-40E5-87CA-62C55481851C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4483F618-9ADB-4A0B-A0D4-37EDB2593F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4483F618-9ADB-4A0B-A0D4-37EDB2593F06}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4483F618-9ADB-4A0B-A0D4-37EDB2593F06}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4483F618-9ADB-4A0B-A0D4-37EDB2593F06}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -51,6 +64,7 @@ Global
|
|||
{F19A2CFF-A4DE-4D84-8220-662EBA16283A} = {0ADFBFB2-69DD-42A9-959C-4B476863594D}
|
||||
{4C7110B5-C596-4AE0-A67F-0AEF0E3D016D} = {F19A2CFF-A4DE-4D84-8220-662EBA16283A}
|
||||
{5F9C587F-9F8A-40E5-87CA-62C55481851C} = {F19A2CFF-A4DE-4D84-8220-662EBA16283A}
|
||||
{6B83FD54-CA1D-4E0A-A700-71AAD1992EF1} = {0ADFBFB2-69DD-42A9-959C-4B476863594D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FD843EB6-8549-43F4-A30F-53A79FF71FA7}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("NSubstitute.Elevated.Tests")]
|
|
@ -3,11 +3,12 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NSubstitute.Core;
|
||||
using NSubstitute.Elevated.Utilities;
|
||||
using NSubstitute.Elevated.WeaverInternals;
|
||||
using NSubstitute.Exceptions;
|
||||
using NSubstitute.Proxies;
|
||||
using NSubstitute.Proxies.CastleDynamicProxy;
|
||||
using NSubstitute.Proxies.DelegateProxy;
|
||||
using Unity.Core;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
|
@ -15,6 +16,7 @@ namespace NSubstitute.Elevated
|
|||
{
|
||||
readonly CallFactory m_CallFactory;
|
||||
readonly IProxyFactory m_DefaultProxyFactory = new ProxyFactory(new DelegateProxyFactory(), new CastleDynamicProxyFactory());
|
||||
readonly object[] k_MockedCtorParams = { new MockPlaceholderType() };
|
||||
|
||||
public ElevatedSubstituteManager(ISubstitutionContext substitutionContext)
|
||||
{
|
||||
|
@ -23,14 +25,17 @@ namespace NSubstitute.Elevated
|
|||
|
||||
object IProxyFactory.GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, object[] constructorArguments)
|
||||
{
|
||||
// TODO:
|
||||
// * new type MockCtorPlaceholder in elevated assy
|
||||
// * generate new empty ctor that takes MockCtorPlaceholder in all mocked types
|
||||
// * support ctor params. throw if foudn and not ForPartsOf. then ForPartsOf determines which ctor we use.
|
||||
// * have a note about static ctors. because they are special, and do not support disposal, can't really mock them right.
|
||||
// best for user to do mock/unmock of static ctors manually (i.e. move into StaticInit/StaticDispose and call directly from test code)
|
||||
|
||||
object proxy;
|
||||
var substituteConfig = ElevatedSubstitutionContext.TryGetSubstituteConfig(callRouter);
|
||||
|
||||
var shouldForward = typeToProxy.IsInterface;
|
||||
|
||||
// TEMP
|
||||
shouldForward |= typeToProxy.FullName != "SystemUnderTest.SimpleClass";
|
||||
|
||||
if (shouldForward)
|
||||
if (typeToProxy.IsInterface || substituteConfig == null)
|
||||
{
|
||||
proxy = m_DefaultProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
|
||||
}
|
||||
|
@ -53,18 +58,18 @@ namespace NSubstitute.Elevated
|
|||
if (additionalInterfaces.Any())
|
||||
throw new SubstituteException("Cannot add interfaces at runtime to patched types");
|
||||
|
||||
// nsubstitute's dynamic proxy works on concrete classes by inheriting via a runtime-generated type, overriding
|
||||
// virtuals with interceptor behavior. because the base is unmodified, it needs ctor params to be passed in
|
||||
// for the proxy to pass to base. this in turn likely runs code, and we're not really working with an actual
|
||||
// mock.
|
||||
//
|
||||
// elevated mocking via assembly patching, by contrast, lets us a) insert a new default ctor where missing, and
|
||||
// b) bypass any existing default ctor code from executing. we end up with a true mock. therefore, it makes no
|
||||
// sense to ever pass in ctor args, so this case becomes an exception.
|
||||
if (constructorArguments.Any())
|
||||
throw new SubstituteException("Do not pass ctor args when substituting with elevated mocks");
|
||||
if (substituteConfig == SubstituteConfig.OverrideAllCalls)
|
||||
{
|
||||
// overriding all calls includes the ctor, so it makes no sense for the user to pass in ctor args
|
||||
if (constructorArguments.Any())
|
||||
throw new SubstituteException("Do not pass ctor args when substituting with elevated mocks (or did you mean to use ForPartsOf?)");
|
||||
|
||||
proxy = CreateProxy(typeToProxy, callRouter);
|
||||
// but we use a ctor arg to select the special empty ctor that we patched in
|
||||
constructorArguments = k_MockedCtorParams;
|
||||
}
|
||||
|
||||
proxy = Activator.CreateInstance(typeToProxy, constructorArguments);
|
||||
GetRouterField(typeToProxy).SetValue(proxy, callRouter);
|
||||
}
|
||||
|
||||
return proxy;
|
||||
|
@ -90,16 +95,6 @@ namespace NSubstitute.Elevated
|
|||
}));
|
||||
}
|
||||
|
||||
object CreateProxy(Type typeToProxy, ICallRouter callRouter)
|
||||
{
|
||||
var field = GetRouterField(typeToProxy);
|
||||
|
||||
var newInstance = Activator.CreateInstance(typeToProxy);
|
||||
field.SetValue(newInstance, callRouter);
|
||||
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
// called from patched assembly code via the PatchedAssemblyBridge. return true if the mock is handling the behavior.
|
||||
// false means that the original implementation should run.
|
||||
public bool TryMock(Type actualType, object instance, Type mockedReturnType, out object mockedReturnValue, MethodInfo method, Type[] methodGenericTypes, object[] args)
|
||||
|
@ -109,7 +104,7 @@ namespace NSubstitute.Elevated
|
|||
|
||||
if (callRouter != null)
|
||||
{
|
||||
bool shouldCallOriginalMethod = false;
|
||||
var shouldCallOriginalMethod = false;
|
||||
var call = m_CallFactory.Create(method, args, instance, () => shouldCallOriginalMethod = true);
|
||||
mockedReturnValue = callRouter.Route(call);
|
||||
|
||||
|
@ -126,10 +121,10 @@ namespace NSubstitute.Elevated
|
|||
// 2. support for struct instances (only possible to associate call routers with individual structs from the inside)
|
||||
// 3. is a simple way to check that a type has been patched
|
||||
//
|
||||
FieldInfo GetStaticRouterField(Type type) => m_RouterStaticFieldCache.GetOrAdd(type, t => GetRouterField(t, "__mock__staticData", BindingFlags.Static));
|
||||
FieldInfo GetRouterField(Type type) => m_RouterFieldCache.GetOrAdd(type, t => GetRouterField(t, "__mock__data", BindingFlags.Instance));
|
||||
FieldInfo GetStaticRouterField(Type type) => m_RouterStaticFieldCache.GetOrAdd(type, t => GetRouterField(t, Weaver.MockInjector.InjectedMockStaticDataName, BindingFlags.Static));
|
||||
FieldInfo GetRouterField(Type type) => m_RouterFieldCache.GetOrAdd(type, t => GetRouterField(t, Weaver.MockInjector.InjectedMockDataName, BindingFlags.Instance));
|
||||
|
||||
static FieldInfo GetRouterField(Type type, string fieldName, BindingFlags bindingFlags)
|
||||
static FieldInfo GetRouterField(IReflect type, string fieldName, BindingFlags bindingFlags)
|
||||
{
|
||||
var field = type.GetField(fieldName, bindingFlags | BindingFlags.NonPublic);
|
||||
if (field == null)
|
||||
|
|
|
@ -3,13 +3,13 @@ using System.Collections.Generic;
|
|||
using JetBrains.Annotations;
|
||||
using NSubstitute.Core;
|
||||
using NSubstitute.Core.Arguments;
|
||||
using NSubstitute.Elevated.Utilities;
|
||||
using NSubstitute.Exceptions;
|
||||
using NSubstitute.Routing;
|
||||
using Unity.Core;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
// class motivation:
|
||||
// motivation:
|
||||
//
|
||||
// 1. it's the clean way to hook in our own proxy factory to the nsub machinery
|
||||
// 2. provide access to the sub manager so patched assemblies can route hooked calls through nsub (the so-called 'elevated' mock part)
|
||||
|
@ -19,12 +19,13 @@ namespace NSubstitute.Elevated
|
|||
readonly ISubstitutionContext m_Forwarder;
|
||||
readonly ISubstituteFactory m_ElevatedSubstituteFactory;
|
||||
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public ElevatedSubstitutionContext([NotNull] ISubstitutionContext forwarder)
|
||||
{
|
||||
m_Forwarder = forwarder;
|
||||
ElevatedSubstituteManager = new ElevatedSubstituteManager(this);
|
||||
m_ElevatedSubstituteFactory = new SubstituteFactory(this,
|
||||
new CallRouterFactory(), ElevatedSubstituteManager, new CallRouterResolver());
|
||||
new ElevatedCallRouterFactory(), ElevatedSubstituteManager, new CallRouterResolver());
|
||||
}
|
||||
|
||||
public static IDisposable AutoHook()
|
||||
|
@ -43,6 +44,23 @@ namespace NSubstitute.Elevated
|
|||
|
||||
internal ElevatedSubstituteManager ElevatedSubstituteManager { get; }
|
||||
|
||||
class ElevatedCallRouterFactory : ICallRouterFactory
|
||||
{
|
||||
public ICallRouter Create(ISubstitutionContext substitutionContext, SubstituteConfig config)
|
||||
=> new ElevatedCallRouter(new SubstituteState(substitutionContext, config), substitutionContext, new RouteFactory());
|
||||
}
|
||||
|
||||
class ElevatedCallRouter : CallRouter
|
||||
{
|
||||
public ElevatedCallRouter(ISubstituteState substituteState, ISubstitutionContext context, IRouteFactory routeFactory)
|
||||
: base(substituteState, context, routeFactory) => SubstituteConfig = substituteState.SubstituteConfig;
|
||||
|
||||
public SubstituteConfig SubstituteConfig { get; }
|
||||
}
|
||||
|
||||
internal static SubstituteConfig? TryGetSubstituteConfig(ICallRouter callRouter)
|
||||
=> (callRouter as ElevatedCallRouter)?.SubstituteConfig;
|
||||
|
||||
// this is the only one we're overriding for now, so we can hook our own factory in there.
|
||||
ISubstituteFactory ISubstitutionContext.SubstituteFactory => m_ElevatedSubstituteFactory;
|
||||
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> @this, TKey key, Func<TKey, TValue> createFunc)
|
||||
{
|
||||
if (@this.TryGetValue(key, out var found))
|
||||
return found;
|
||||
|
||||
found = createFunc(key);
|
||||
@this.Add(key, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
public static object GetDefaultValue(this Type @this)
|
||||
{
|
||||
object defaultValue = null;
|
||||
if (@this.IsValueType && @this != typeof(void))
|
||||
defaultValue = Activator.CreateInstance(@this);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static bool NullOrEmpty<T>(this IEnumerable<T> @this)
|
||||
{
|
||||
return @this == null || !@this.Any();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,11 +8,18 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations">
|
||||
<Version>11.0.0</Version>
|
||||
<Version>11.1.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NSubstitute">
|
||||
<Version>2.0.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Mono.Cecil">
|
||||
<Version>0.10.0-beta6</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Unity.Core\Unity.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -3,11 +3,15 @@ using System.Diagnostics;
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using NSubstitute.Core;
|
||||
using Unity.Core;
|
||||
|
||||
// this namespace contains types that must be public in order to be usable from patched assemblies, yet
|
||||
// we do not want used from normal client api
|
||||
namespace NSubstitute.Elevated.WeaverInternals
|
||||
{
|
||||
// used when generating mocked default ctors
|
||||
public class MockPlaceholderType {}
|
||||
|
||||
// important: keep all non-mscorlib types out of the public surface area of this class, so as to
|
||||
// avoid needing to add more references than NSubstitute.Elevated to the assembly during patching.
|
||||
|
||||
|
|
|
@ -1,17 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace NSubstitute.Elevated.Utilities
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
public class DelegateDisposable : IDisposable
|
||||
{
|
||||
readonly Action m_DisposeAction;
|
||||
|
||||
public DelegateDisposable([NotNull] Action disposeAction) => m_DisposeAction = disposeAction;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_DisposeAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Mono.Cecil;
|
||||
using Unity.Core;
|
||||
|
||||
namespace NSubstitute.Elevated.Weaver
|
||||
{
|
||||
public enum IncludeNested { No, Yes }
|
||||
|
||||
public static class CecilExtensions
|
||||
{
|
||||
[NotNull]
|
||||
public static IEnumerable<TypeDefinition> SelectTypes([NotNull] this AssemblyDefinition @this, IncludeNested includeNested)
|
||||
{
|
||||
var types = @this.Modules.SelectMany(m => m.Types);
|
||||
if (includeNested == IncludeNested.Yes)
|
||||
types = types.SelectMany(t => t.NestedTypes.Append(t));
|
||||
return types;
|
||||
}
|
||||
|
||||
public static int InheritanceChainLength([NotNull] this TypeReference @this)
|
||||
{
|
||||
if (@this.DeclaringType == null)
|
||||
return 0;
|
||||
|
||||
var baseType = @this.Resolve().BaseType;
|
||||
if (baseType == null)
|
||||
return 1;
|
||||
|
||||
return 1 + InheritanceChainLength(baseType);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using Mono.Cecil;
|
||||
using Unity.Core;
|
||||
|
||||
namespace NSubstitute.Elevated.Weaver
|
||||
{
|
||||
public enum PatchTestAssembly { No, Yes }
|
||||
|
||||
public static class ElevatedWeaver
|
||||
{
|
||||
const string k_PatchBackupExtension = ".orig";
|
||||
|
||||
public static string GetPatchBackupPathFor(string path)
|
||||
=> path + k_PatchBackupExtension;
|
||||
|
||||
public static IReadOnlyCollection<PatchResult> PatchAllDependentAssemblies(
|
||||
[NotNull] string testAssemblyPath,
|
||||
PatchTestAssembly patchTestAssembly = PatchTestAssembly.No) // typically we don't want to patch the test assembly itself, only the systems under test
|
||||
{
|
||||
var testAssemblyFolder = Path.GetDirectoryName(testAssemblyPath);
|
||||
if (testAssemblyFolder.IsNullOrEmpty())
|
||||
throw new Exception("Unable to find folder for test assembly");
|
||||
testAssemblyFolder = Path.GetFullPath(testAssemblyFolder);
|
||||
|
||||
// scope
|
||||
{
|
||||
var thisAssemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
if (thisAssemblyFolder.IsNullOrEmpty())
|
||||
throw new Exception("Can only patch assemblies on disk");
|
||||
thisAssemblyFolder = Path.GetFullPath(thisAssemblyFolder);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
var nsubElevatedPath = Path.Combine(testAssemblyFolder, "NSubstitute.Elevated.dll");
|
||||
using (var nsubElevatedAssembly = AssemblyDefinition.ReadAssembly(nsubElevatedPath))
|
||||
{
|
||||
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)
|
||||
{
|
||||
var assemblyToPatchPath = toProcess[toProcessIndex];
|
||||
if (patchResults.ContainsKey(assemblyToPatchPath))
|
||||
continue;
|
||||
|
||||
if (!Path.IsPathRooted(assemblyToPatchPath))
|
||||
throw new Exception($"Unexpected non-rooted assembly path '{assemblyToPatchPath}'");
|
||||
|
||||
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");
|
||||
|
||||
if (File.Exists(foundPath))
|
||||
toProcess.Add(foundPath);
|
||||
else if (!patchResults.ContainsKey(referencedAssembly.Name))
|
||||
patchResults.Add(referencedAssembly.Name, new PatchResult(referencedAssembly.Name, null, PatchState.IgnoredOutsideAllowedPaths));
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
mockInjector.Patch(assemblyToPatch);
|
||||
|
||||
// atomic write of file with backup
|
||||
var tmpPath = assemblyToPatchPath + ".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);
|
||||
// $$$ TODO: move pdb file too
|
||||
|
||||
patchResult = new PatchResult(assemblyToPatchPath, originalPath, PatchState.Patched);
|
||||
}
|
||||
|
||||
patchResults.Add(assemblyToPatchPath, patchResult);
|
||||
}
|
||||
}
|
||||
|
||||
return patchResults.Values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum PatchState
|
||||
{
|
||||
GeneralFailure, // something else went wrong
|
||||
IgnoredTestAssembly, // don't patch the test assembly itself, as we're requiring that to always be separate from the systems under test
|
||||
IgnoredOutsideAllowedPaths, // don't want to patch things that are not "ours"
|
||||
//AlreadyPatchedOld, // assy already patched against an older set of tooling TODO: implement
|
||||
AlreadyPatched, // assy already patched against current tooling
|
||||
Patched, // assy patched and old one backed up
|
||||
}
|
||||
|
||||
public struct PatchResult
|
||||
{
|
||||
public string Path;
|
||||
public string OriginalPath;
|
||||
public PatchState PatchState;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public PatchResult(string path, string originalPath, PatchState patchState)
|
||||
{
|
||||
Path = path;
|
||||
OriginalPath = originalPath;
|
||||
PatchState = patchState;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Policy;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Cecil.Rocks;
|
||||
using NSubstitute.Elevated.WeaverInternals;
|
||||
using Unity.Core;
|
||||
using Assembly = System.Reflection.Assembly;
|
||||
using AssemblyMetadataAttribute = System.Reflection.AssemblyMetadataAttribute;
|
||||
|
||||
namespace NSubstitute.Elevated.Weaver
|
||||
{
|
||||
class MockInjector
|
||||
{
|
||||
static readonly string k_MarkAsPatchedKey, k_MarkAsPatchedValue;
|
||||
|
||||
readonly TypeDefinition m_MockPlaceholderType;
|
||||
readonly MethodDefinition m_PatchedAssemblyBridgeTryMock;
|
||||
|
||||
public const string InjectedMockStaticDataName = "__mock__staticData", InjectedMockDataName = "__mock__data";
|
||||
|
||||
static MockInjector()
|
||||
{
|
||||
k_MarkAsPatchedKey = Assembly.GetExecutingAssembly().GetName().Name;
|
||||
|
||||
var assemblyHash = Assembly.GetExecutingAssembly().Evidence.GetHostEvidence<Hash>();
|
||||
if (assemblyHash == null)
|
||||
throw new Exception("Assembly not stamped with a hash");
|
||||
|
||||
k_MarkAsPatchedValue = assemblyHash.SHA1.ToHexString();
|
||||
}
|
||||
|
||||
public MockInjector(AssemblyDefinition nsubElevatedAssembly)
|
||||
{
|
||||
m_MockPlaceholderType = nsubElevatedAssembly.MainModule
|
||||
.GetType(typeof(MockPlaceholderType).FullName);
|
||||
|
||||
m_PatchedAssemblyBridgeTryMock = nsubElevatedAssembly.MainModule
|
||||
.GetType(typeof(PatchedAssemblyBridge).FullName)
|
||||
.Methods.Single(m => m.Name == nameof(PatchedAssemblyBridge.TryMock));
|
||||
}
|
||||
|
||||
public void Patch(AssemblyDefinition assembly)
|
||||
{
|
||||
// patch all types
|
||||
|
||||
var typesToProcess = assembly
|
||||
.SelectTypes(IncludeNested.Yes)
|
||||
.OrderBy(t => t.InheritanceChainLength()) // process base classes first
|
||||
.ToList(); // copy to a list in case patch work we do would invalidate the enumerator
|
||||
|
||||
foreach (var type in typesToProcess)
|
||||
Patch(type);
|
||||
|
||||
// add an attr to mark the assembly as patched
|
||||
|
||||
var mainModule = assembly.MainModule;
|
||||
var types = mainModule.TypeSystem;
|
||||
|
||||
var metadataAttrName = typeof(AssemblyMetadataAttribute);
|
||||
var metadataAttrType = new TypeReference(metadataAttrName.Namespace, metadataAttrName.Name, mainModule, types.CoreLibrary);
|
||||
var metadataAttrCtor = new MethodReference(".ctor", types.Void, metadataAttrType) { HasThis = true };
|
||||
metadataAttrCtor.Parameters.Add(new ParameterDefinition(types.String));
|
||||
metadataAttrCtor.Parameters.Add(new ParameterDefinition(types.String));
|
||||
|
||||
var metadataAttr = new CustomAttribute(metadataAttrCtor);
|
||||
metadataAttr.ConstructorArguments.Add(new CustomAttributeArgument(types.String, k_MarkAsPatchedKey));
|
||||
metadataAttr.ConstructorArguments.Add(new CustomAttributeArgument(types.String, k_MarkAsPatchedValue));
|
||||
|
||||
assembly.CustomAttributes.Add(metadataAttr);
|
||||
}
|
||||
|
||||
public static bool IsPatched(AssemblyDefinition assembly)
|
||||
{
|
||||
return assembly.CustomAttributes.Any(a =>
|
||||
a.AttributeType.FullName == typeof(AssemblyMetadataAttribute).FullName &&
|
||||
a.ConstructorArguments.Count == 2 &&
|
||||
a.ConstructorArguments[0].Value as string == k_MarkAsPatchedKey &&
|
||||
a.ConstructorArguments[1].Value as string == k_MarkAsPatchedValue);
|
||||
}
|
||||
|
||||
public static bool IsPatched(string assemblyPath)
|
||||
{
|
||||
using (var assembly = AssemblyDefinition.ReadAssembly(assemblyPath))
|
||||
return IsPatched(assembly);
|
||||
}
|
||||
|
||||
void Patch(TypeDefinition type)
|
||||
{
|
||||
if (type.IsInterface)
|
||||
return;
|
||||
if (type.IsNestedPrivate)
|
||||
return;
|
||||
if (type.Name == "<Module>")
|
||||
return;
|
||||
if (type.CustomAttributes.Any(a =>
|
||||
a.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName ||
|
||||
a.AttributeType.FullName == typeof(StructLayoutAttribute).FullName))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var patched = false;
|
||||
foreach (var method in type.Methods)
|
||||
{
|
||||
if (Patch(method))
|
||||
patched = true;
|
||||
}
|
||||
|
||||
if (patched)
|
||||
{
|
||||
void AddField(string fieldName, FieldAttributes fieldAttributes)
|
||||
{
|
||||
type.Fields.Add(new FieldDefinition(fieldName,
|
||||
FieldAttributes.Private | FieldAttributes.NotSerialized | fieldAttributes,
|
||||
type.Module.TypeSystem.Object));
|
||||
}
|
||||
|
||||
AddField(InjectedMockStaticDataName, FieldAttributes.Static);
|
||||
AddField(InjectedMockDataName, 0);
|
||||
|
||||
AddMockCtor(type);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception($"Internal error during mock injection into type {type.FullName}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsPatched(TypeDefinition type)
|
||||
{
|
||||
var mockStaticField = type.Fields.SingleOrDefault(f => f.Name == InjectedMockStaticDataName);
|
||||
var mockField = type.Fields.SingleOrDefault(f => f.Name == InjectedMockDataName);
|
||||
if ((mockStaticField != null) != (mockField != null))
|
||||
throw new Exception("Unexpected mismatch between static and instance mock injected fields");
|
||||
|
||||
return mockStaticField != null;
|
||||
}
|
||||
|
||||
void AddMockCtor(TypeDefinition type)
|
||||
{
|
||||
var ctor = new MethodDefinition(".ctor",
|
||||
MethodAttributes.Public | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
|
||||
type.Module.TypeSystem.Void)
|
||||
{
|
||||
IsManaged = true,
|
||||
DeclaringType = type,
|
||||
HasThis = true,
|
||||
};
|
||||
ctor.Parameters.Add(new ParameterDefinition(type.Module.ImportReference(m_MockPlaceholderType)));
|
||||
|
||||
var body = ctor.Body;
|
||||
body.Instructions.Clear();
|
||||
|
||||
var il = body.GetILProcessor();
|
||||
|
||||
var baseCtor = (MethodReference)type
|
||||
.BaseType.Resolve()
|
||||
.GetConstructors()
|
||||
.SingleOrDefault(candidate => candidate.Parameters.SequenceEqual(ctor.Parameters));
|
||||
if (baseCtor != null)
|
||||
{
|
||||
if (type.BaseType.IsGenericInstance)
|
||||
baseCtor = new MethodReference(baseCtor.Name, baseCtor.ReturnType, type.BaseType) { HasThis = baseCtor.HasThis };
|
||||
else if (baseCtor.Module != type.Module)
|
||||
baseCtor = type.Module.ImportReference(baseCtor);
|
||||
|
||||
il.Append(il.Create(OpCodes.Ldarg_0));
|
||||
il.Append(il.Create(OpCodes.Call, baseCtor));
|
||||
}
|
||||
|
||||
il.Append(il.Create(OpCodes.Ret));
|
||||
|
||||
type.Methods.Add(ctor);
|
||||
}
|
||||
|
||||
bool Patch(MethodDefinition method)
|
||||
{
|
||||
if (method.IsCompilerControlled || method.IsConstructor || method.IsAbstract)
|
||||
return false;
|
||||
|
||||
// $$$ DOWIT
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace Unity.Core.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DictionaryExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void OrEmpty_NonNullInput_ReturnsInput()
|
||||
{
|
||||
var dictionary = new Dictionary<int, string> {[0] = "zero" };
|
||||
|
||||
dictionary.OrEmpty().ShouldBe(dictionary);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OrEmpty_NullInput_ReturnsEmpty()
|
||||
{
|
||||
IReadOnlyDictionary<string, int> dictionary = null;
|
||||
|
||||
dictionary.OrEmpty().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetValueOr_Found_ReturnsFound()
|
||||
{
|
||||
var dictionary = new Dictionary<int, string> {[1] = "one" };
|
||||
|
||||
dictionary.GetValueOr(1).ShouldBe("one");
|
||||
dictionary.GetValueOr(1, "two").ShouldBe("one");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetValueOr_NotFound_ReturnsDefault()
|
||||
{
|
||||
var dictionary = new Dictionary<string, int> {["one"] = 1 };
|
||||
|
||||
dictionary.GetValueOr("two").ShouldBe(0);
|
||||
dictionary.GetValueOr("two", 2).ShouldBe(2);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
using Unity.Core;
|
||||
|
||||
namespace Unity.Core.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DiffUtilsTests
|
||||
{
|
||||
[Test]
|
||||
public void IsDiff_ValidLfDiff_ReturnsTrue()
|
||||
{
|
||||
var diffText = new[]
|
||||
{
|
||||
"--- a/cppupdatr/Refactor/MoveFile.cs",
|
||||
"+++ b/cppupdatr/Refactor/MoveFile.cs",
|
||||
"@@ -1,6 +1,7 @@",
|
||||
}.StringJoin('\n');
|
||||
|
||||
DiffUtils.IsDiff(diffText).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsDiff_ValidCrLfDiff_ReturnsTrue()
|
||||
{
|
||||
var diffText = new[]
|
||||
{
|
||||
"--- a/cppupdatr/Refactor/MoveFile.cs",
|
||||
"+++ b/cppupdatr/Refactor/MoveFile.cs",
|
||||
"@@ -1,6 +1,7 @@",
|
||||
}.StringJoin("\r\n");
|
||||
|
||||
DiffUtils.IsDiff(diffText).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsDiff_EmptyDiff_ReturnsFalse()
|
||||
{
|
||||
DiffUtils.IsDiff("").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsDiff_BrokenDiff_ReturnsFalse()
|
||||
{
|
||||
var diffText = new[]
|
||||
{
|
||||
"--- a/cppupdatr/Refactor/MoveFile.cs",
|
||||
" +++ b/cppupdatr/Refactor/MoveFile.cs",
|
||||
"@@ -1,6 +1,7 @@"
|
||||
}.StringJoin('\n');
|
||||
|
||||
DiffUtils.IsDiff(diffText).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsDiff_IncompleteDiff_ReturnsFalse()
|
||||
{
|
||||
var diffText = new[]
|
||||
{
|
||||
"--- a/cppupdatr/Refactor/MoveFile.cs",
|
||||
"+++ b/cppupdatr/Refactor/MoveFile.cs",
|
||||
}.StringJoin('\n');
|
||||
|
||||
DiffUtils.IsDiff(diffText).ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace Unity.Core.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class EnumerableExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void WhereNotNull_ItemsWithNulls_ReturnsFilteredForNull()
|
||||
{
|
||||
var dummy1 = Enumerable.Empty<float>();
|
||||
var dummy2 = new Exception();
|
||||
var enumerable = new object[] { null, "abc", dummy1, dummy2, null, null, "ghi" };
|
||||
|
||||
enumerable.WhereNotNull().ShouldBe(new object[] { "abc", dummy1, dummy2, "ghi" });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhereNotNull_Empty_ReturnsEmpty()
|
||||
{
|
||||
var enumerable = Enumerable.Empty<Exception>();
|
||||
|
||||
enumerable.WhereNotNull().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhereNotNull_AllNulls_ReturnsEmpty()
|
||||
{
|
||||
var enumerable = new object[] { null, null, null };
|
||||
|
||||
enumerable.WhereNotNull().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OrEmpty_NonNullInput_ReturnsInput()
|
||||
{
|
||||
var enumerable = new string[0];
|
||||
|
||||
enumerable.OrEmpty().ShouldBe(enumerable);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OrEmpty_NullInput_ReturnsEmpty()
|
||||
{
|
||||
IEnumerable<string> enumerable = null;
|
||||
|
||||
enumerable.OrEmpty().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToDictionary_Tuples_ReturnsMappedDictionary()
|
||||
{
|
||||
var items = new[] { (1, "one"), (2, "two") };
|
||||
var dictionary = items.ToDictionary();
|
||||
|
||||
dictionary[1].ShouldBe("one");
|
||||
dictionary[2].ShouldBe("two");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToDictionary_TuplesWithDups_Throws()
|
||||
{
|
||||
var items = new[] { (1, "one"), (1, "two") };
|
||||
Should.Throw<Exception>(() => items.ToDictionary());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
// ReSharper disable AssignNullToNotNullAttribute
|
||||
// ReSharper disable ExpressionIsAlwaysNull
|
||||
|
||||
namespace Unity.Core.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class RefTypeExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void WrapEnumerables_NonNullInput_ReturnsInputWrappedInEnumerable()
|
||||
{
|
||||
const string item = "test";
|
||||
|
||||
var enumerable = item.WrapInEnumerable();
|
||||
enumerable.ShouldBe(new[] { item });
|
||||
|
||||
enumerable = item.WrapInEnumerableOrEmpty();
|
||||
enumerable.ShouldBe(new[] { item });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WrapEnumerable_NullInput_ReturnsNullWrappedInEnumerable()
|
||||
{
|
||||
string item = null;
|
||||
var enumerable = item.WrapInEnumerable();
|
||||
enumerable.ShouldBe(new[] { item });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WrapEnumerableOrEmpty_NullInput_ReturnsEmptyEnumerable()
|
||||
{
|
||||
string item = null;
|
||||
var enumerable = item.WrapInEnumerableOrEmpty();
|
||||
enumerable.ShouldBe(Enumerable.Empty<string>());
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public class ComparableExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void Clamp_BadRange_ShouldThrow()
|
||||
{
|
||||
Should.Throw<Exception>(() => 1.Clamp (2, 1));
|
||||
Should.Throw<Exception>(() => 'a'.Clamp('z', 'y'));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clamp_InBounds_ReturnsValue()
|
||||
{
|
||||
5.Clamp (2, 10).ShouldBe(5);
|
||||
3.14.Clamp(3, 6).ShouldBe(3.14);
|
||||
'b'.Clamp('a', 'z').ShouldBe('b');
|
||||
"abc".Clamp("a", "b").ShouldBe("abc");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clamp_OutOfBounds_ReturnsClampedValue()
|
||||
{
|
||||
15.Clamp (3, 12).ShouldBe(12);
|
||||
(-5).Clamp(-2, 4).ShouldBe(-2);
|
||||
|
||||
3.14.Clamp(3.2, 4.3).ShouldBe(3.2);
|
||||
(-3.24).Clamp(-2.1, 1.5).ShouldBe(-2.1);
|
||||
|
||||
'b'.Clamp('d', 'z').ShouldBe('d');
|
||||
'f'.Clamp('a', 'c').ShouldBe('c');
|
||||
|
||||
"abc".Clamp("bde", "cde").ShouldBe("bde");
|
||||
"hi".Clamp("abc", "foo").ShouldBe("foo");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clamp_Integer_ReturnsInclusiveClampedValue()
|
||||
{
|
||||
5.Clamp (0, 5).ShouldBe(5);
|
||||
5.Clamp (0, 5).ShouldNotBe(4);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
using System;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace Unity.Core.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class StringExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void Left_InBounds_ReturnsSubstring()
|
||||
{
|
||||
"".Left(0).ShouldBe("");
|
||||
"abc".Left(2).ShouldBe("ab");
|
||||
"abc".Left(0).ShouldBe("");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Left_OutOfBounds_ClampsProperly()
|
||||
{
|
||||
"".Left(10).ShouldBe("");
|
||||
"abc".Left(10).ShouldBe("abc");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Left_BadInput_Throws()
|
||||
{
|
||||
// ReSharper disable once AssignNullToNotNullAttribute
|
||||
Should.Throw<Exception>(() => ((string)null).Left(1));
|
||||
Should.Throw<Exception>(() => "abc".Left(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Mid_InBounds_ReturnsSubstring()
|
||||
{
|
||||
"".Mid(0, 0).ShouldBe("");
|
||||
"abc".Mid(0, 3).ShouldBe("abc");
|
||||
"abc".Mid(0).ShouldBe("abc");
|
||||
"abc".Mid(0, -2).ShouldBe("abc");
|
||||
"abc".Mid(1, 1).ShouldBe("b");
|
||||
"abc".Mid(3, 0).ShouldBe("");
|
||||
"abc".Mid(0, 0).ShouldBe("");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Mid_OutOfBounds_ClampsProperly()
|
||||
{
|
||||
"".Mid(10, 5).ShouldBe("");
|
||||
"abc".Mid(0, 10).ShouldBe("abc");
|
||||
"abc".Mid(1, 10).ShouldBe("bc");
|
||||
"abc".Mid(10, 5).ShouldBe("");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Mid_BadInput_Throws()
|
||||
{
|
||||
// ReSharper disable once AssignNullToNotNullAttribute
|
||||
Should.Throw<Exception>(() => ((string)null).Mid(1, 2));
|
||||
Should.Throw<Exception>(() => "abc".Mid(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Right_InBounds_ReturnsSubstring()
|
||||
{
|
||||
"".Right(0).ShouldBe("");
|
||||
"abc".Right(2).ShouldBe("bc");
|
||||
"abc".Right(0).ShouldBe("");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Right_OutOfBounds_ClampsProperly()
|
||||
{
|
||||
"".Right(10).ShouldBe("");
|
||||
"abc".Right(10).ShouldBe("abc");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Right_BadInput_Throws()
|
||||
{
|
||||
// ReSharper disable once AssignNullToNotNullAttribute
|
||||
Should.Throw<Exception>(() => ((string)null).Right(1));
|
||||
Should.Throw<Exception>(() => "abc".Right(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringJoin_WithEmpty_ReturnsEmptyString()
|
||||
{
|
||||
var enumerable = new object[0];
|
||||
|
||||
enumerable.StringJoin(", ").ShouldBe("");
|
||||
enumerable.StringJoin(';').ShouldBe("");
|
||||
enumerable.StringJoin(o => o, ", ").ShouldBe("");
|
||||
enumerable.StringJoin(o => o, ';').ShouldBe("");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringJoin_WithSingle_ReturnsNoSeparators()
|
||||
{
|
||||
var enumerable = new[] { "abc" };
|
||||
|
||||
enumerable.StringJoin(", ").ShouldBe("abc");
|
||||
enumerable.StringJoin(';').ShouldBe("abc");
|
||||
enumerable.StringJoin(o => o, ", ").ShouldBe("abc");
|
||||
enumerable.StringJoin(o => o, ';').ShouldBe("abc");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringJoin_WithMultiple_ReturnsJoined()
|
||||
{
|
||||
var enumerable = new object[] { "abc", 0b111001, -14, 'z' };
|
||||
|
||||
enumerable.StringJoin(" ==> ").ShouldBe("abc ==> 57 ==> -14 ==> z");
|
||||
enumerable.StringJoin('\n').ShouldBe("abc\n57\n-14\nz");
|
||||
enumerable.StringJoin(o => o, " <> ").ShouldBe("abc <> 57 <> -14 <> z");
|
||||
enumerable.StringJoin(o => o, ';').ShouldBe("abc;57;-14;z");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringJoin_WithSelectorAndSimpleEnumerable_ReturnsSelectedJoined()
|
||||
{
|
||||
var enumerable = new[] { "hi", "there", "this", "", "is", "some", "stuff" };
|
||||
|
||||
int Selector(string value) { return value.Length; }
|
||||
|
||||
enumerable.StringJoin(Selector, ", ").ShouldBe("2, 5, 4, 0, 2, 4, 5");
|
||||
enumerable.StringJoin(Selector, ';').ShouldBe("2;5;4;0;2;4;5");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringJoin_WithSelectorAndComplexEnumerable_ReturnsSelectedJoined()
|
||||
{
|
||||
var enumerable = new object[] { "abc", 123, null, ("hi", 1.23) };
|
||||
|
||||
string Selector(object value) { return value?.GetType().Name ?? "(null)"; }
|
||||
|
||||
enumerable.StringJoin(Selector, " ** ").ShouldBe("String ** Int32 ** (null) ** ValueTuple`2");
|
||||
enumerable.StringJoin(Selector, '?').ShouldBe("String?Int32?(null)?ValueTuple`2");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="../common.targets" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations">
|
||||
<Version>11.1.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NUnit">
|
||||
<Version>3.8.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly">
|
||||
<Version>2.8.3</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Unity.Core\Unity.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public static class Stdin
|
||||
{
|
||||
public static IEnumerable<string> SelectLines()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
var line = Console.ReadLine();
|
||||
if (line == null)
|
||||
yield break;
|
||||
|
||||
yield return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public class ReadOnlyDictionary
|
||||
{
|
||||
class EmptyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
|
||||
{
|
||||
public static readonly IReadOnlyDictionary<TKey, TValue> instance = new EmptyDictionary<TKey, TValue>();
|
||||
|
||||
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => Enumerable.Empty<KeyValuePair<TKey, TValue>>().GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
public int Count => 0;
|
||||
public bool ContainsKey(TKey key) => false;
|
||||
public bool TryGetValue(TKey key, out TValue value) { value = default(TValue); return false; }
|
||||
public TValue this[TKey key] => throw new KeyNotFoundException();
|
||||
public IEnumerable<TKey> Keys => Enumerable.Empty<TKey>();
|
||||
public IEnumerable<TValue> Values => Enumerable.Empty<TValue>();
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<TKey, TValue> Empty<TKey, TValue>()
|
||||
=> EmptyDictionary<TKey, TValue>.instance;
|
||||
}
|
||||
|
||||
public static class Dictionary
|
||||
{
|
||||
[NotNull]
|
||||
public static Dictionary<TKey, TValue> Create<TKey, TValue>(params(TKey key, TValue value)[] items)
|
||||
=> items.ToDictionary();
|
||||
}
|
||||
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
[NotNull]
|
||||
public static IReadOnlyDictionary<TKey, TValue> OrEmpty<TKey, TValue>([CanBeNull] this IReadOnlyDictionary<TKey, TValue> @this)
|
||||
=> @this ?? ReadOnlyDictionary.Empty<TKey, TValue>();
|
||||
|
||||
public static TValue GetValueOr<TKey, TValue>([NotNull] this IReadOnlyDictionary<TKey, TValue> @this, TKey key, TValue defaultValue = default(TValue))
|
||||
=> @this.TryGetValue(key, out var value) ? value : defaultValue;
|
||||
|
||||
public static TValue GetOrAdd<TKey, TValue>([NotNull] this IDictionary<TKey, TValue> @this, TKey key, [NotNull] Func<TKey, TValue> createFunc)
|
||||
{
|
||||
if (@this.TryGetValue(key, out var found))
|
||||
return found;
|
||||
|
||||
found = createFunc(key);
|
||||
@this.Add(key, found);
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public static class DiffUtils
|
||||
{
|
||||
public static bool IsDiff(string candidate)
|
||||
{
|
||||
const string detectDiffPattern = @"(?mx)
|
||||
^
|
||||
---\ [^\n]+\n
|
||||
\+\+\+\ [^\n]+\n
|
||||
@@\ ";
|
||||
|
||||
return Regex.IsMatch(candidate, detectDiffPattern);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public class DelegateDisposable : IDisposable
|
||||
{
|
||||
readonly Action m_DisposeAction;
|
||||
|
||||
public DelegateDisposable([NotNull] Action disposeAction) => m_DisposeAction = disposeAction;
|
||||
public void Dispose() => m_DisposeAction();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Enumerable = System.Linq.Enumerable;
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
[NotNull]
|
||||
public static IEnumerable<T> WhereNotNull<T>([NotNull] this IEnumerable<T> @this) where T : class
|
||||
=> @this.Where(item => !(item is null));
|
||||
|
||||
[NotNull]
|
||||
public static IEnumerable<T> OrEmpty<T>([CanBeNull] this IEnumerable<T> @this)
|
||||
=> @this ?? Enumerable.Empty<T>();
|
||||
|
||||
[NotNull]
|
||||
public static HashSet<T> ToHashSet<T>([NotNull] this IEnumerable<T> @this, IEqualityComparer<T> comparer)
|
||||
=> new HashSet<T>(@this, comparer);
|
||||
|
||||
[NotNull]
|
||||
public static HashSet<T> ToHashSet<T>([NotNull] this IEnumerable<T> @this)
|
||||
=> new HashSet<T>(@this);
|
||||
|
||||
[NotNull]
|
||||
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>([NotNull] this IEnumerable<(TKey key, TValue value)> @this)
|
||||
=> @this.ToDictionary(item => item.key, item => item.value);
|
||||
|
||||
public static IEnumerable<T> Append<T>([NotNull] this IEnumerable<T> @this, T value)
|
||||
{
|
||||
foreach (var i in @this)
|
||||
yield return i;
|
||||
yield return value;
|
||||
}
|
||||
|
||||
public static IEnumerable<T> Prepend<T>([NotNull] this IEnumerable<T> @this, T value)
|
||||
{
|
||||
yield return value;
|
||||
foreach (var i in @this)
|
||||
yield return i;
|
||||
}
|
||||
|
||||
public static bool IsNullOrEmpty<T>([CanBeNull] this IEnumerable<T> @this)
|
||||
=> @this == null || !@this.Any();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public static class LegacyExtensions
|
||||
{
|
||||
public static IEnumerable<Match> FixLegacy([NotNull] this MatchCollection @this)
|
||||
=> @this.Cast<Match>();
|
||||
}
|
||||
|
||||
public static class ObjectExtensions
|
||||
{
|
||||
// fluent operators - note that we're limiting to ref types where needed to avoid accidental boxing
|
||||
|
||||
public static T ToBase<T>(this T @this) => @this; // sometimes you need an inline upcast
|
||||
|
||||
public static T To<T>(this object @this) where T : class => (T)@this;
|
||||
public static T As<T>(this object @this) where T : class => @this as T;
|
||||
public static bool Is<T>(this object @this) where T : class => @this is T;
|
||||
public static bool IsNot<T>(this object @this) where T : class => !(@this is T);
|
||||
}
|
||||
|
||||
public static class RefTypeExtensions
|
||||
{
|
||||
[NotNull]
|
||||
public static IEnumerable<T> WrapInEnumerable<T>(this T @this)
|
||||
{ yield return @this; }
|
||||
|
||||
[NotNull]
|
||||
public static IEnumerable<T> WrapInEnumerableOrEmpty<T>([CanBeNull] this T @this) where T : class
|
||||
=> !(@this is null) ? WrapInEnumerable(@this) : Enumerable.Empty<T>();
|
||||
}
|
||||
|
||||
public static class TypeExtensions
|
||||
{
|
||||
public static object GetDefaultValue([NotNull] this Type @this)
|
||||
{
|
||||
object defaultValue = null;
|
||||
if (@this.IsValueType && @this != typeof(void))
|
||||
defaultValue = Activator.CreateInstance(@this);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ComparableExtensions
|
||||
{
|
||||
public static T Clamp<T>([NotNull] this T @this, T min, T max) where T : IComparable<T>
|
||||
{
|
||||
if (min.CompareTo(max) > 0)
|
||||
throw new ArgumentException("'min' cannot be greater than 'max'", nameof(min));
|
||||
|
||||
if (@this.CompareTo(min) < 0) return min;
|
||||
if (@this.CompareTo(max) > 0) return max;
|
||||
return @this;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ByteArrayExtensions
|
||||
{
|
||||
// if you want to speed this up, see https://stackoverflow.com/q/311165/14582
|
||||
public static string ToHexString([NotNull] this byte[] @this)
|
||||
=> BitConverter.ToString(@this).Replace("-", "");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
|
||||
#if WIP
|
||||
|
||||
// the intention here is to provide something similar to ITreeEnumerable, part of an
|
||||
// ability to write abstract graph algorithms and then easily map onto types with
|
||||
// existing parent structures.
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public interface IHasParent<out T>
|
||||
{
|
||||
T Parent { get; }
|
||||
}
|
||||
|
||||
public class HasParentDelegate<T> : IHasParent<T>
|
||||
{
|
||||
Func<T, T> m_ParentGetter;
|
||||
|
||||
public HasParentDelegate(T @this, Func<T, T> parentGetter)
|
||||
{
|
||||
This = @this;
|
||||
m_ParentGetter = parentGetter;
|
||||
}
|
||||
|
||||
public T Parent => m_ParentGetter(This);
|
||||
public T This { get; }
|
||||
}
|
||||
|
||||
public static class HasParentDelegate
|
||||
{
|
||||
public static HasParentDelegate<T> Create<T>(T @this, Func<T, T> parentGetter)
|
||||
=> new HasParentDelegate<T>(@this, parentGetter);
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public static class SafeFile
|
||||
{
|
||||
// TODO: add tests (see https://stackoverflow.com/a/1528151/14582)
|
||||
public static void AtomicWrite(string path, Action<string> write)
|
||||
{
|
||||
// note that File.Delete doesn't throw if file doesn't exist
|
||||
|
||||
// dotnet doesn't have an atomic move operation (have to pinvoke to something in the OS to get that,
|
||||
// and even then on windows it's not guaranteed). so the "atomic" part of this name is just to ensure
|
||||
// that partially written file never happens.
|
||||
|
||||
var tmpPath = path + ".tmp";
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(tmpPath);
|
||||
write(tmpPath);
|
||||
|
||||
// temporarily keep the old file, until we're sure the new file is moved
|
||||
var bakPath = path + ".bak";
|
||||
File.Delete(bakPath);
|
||||
File.Move(path, bakPath);
|
||||
|
||||
File.Move(tmpPath, path);
|
||||
|
||||
// now the old one can go away
|
||||
// FUTURE: based on option to func, keep bak file
|
||||
File.Delete(bakPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(tmpPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// failure to cleanup a tmp file isn't critical
|
||||
}
|
||||
}
|
||||
|
||||
// FUTURE: options to throw on existing/auto-overwrite
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Unity.Core
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
[ContractAnnotation("null=>true", true), Pure]
|
||||
public static bool IsNullOrEmpty([CanBeNull] this string @this) => string.IsNullOrEmpty(@this);
|
||||
[ContractAnnotation("null=>true", true), Pure]
|
||||
public static bool IsNullOrWhiteSpace([CanBeNull] this string @this) => string.IsNullOrWhiteSpace(@this);
|
||||
|
||||
public static bool IsEmpty([NotNull] this string @this) => @this.Length == 0;
|
||||
public static bool Any([NotNull] this string @this) => @this.Length != 0;
|
||||
|
||||
// left/mid/right are 'basic' inspired names, and never throw
|
||||
|
||||
[NotNull]
|
||||
public static string Left([NotNull] this string @this, int maxChars)
|
||||
{
|
||||
return @this.Substring(0, Math.Min(maxChars, @this.Length));
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
public static string Mid([NotNull] this string @this, int offset, int maxChars = -1)
|
||||
{
|
||||
if (offset < 0)
|
||||
throw new ArgumentException("offset must be >= 0", nameof(offset));
|
||||
|
||||
var safeOffset = offset.Clamp(0, @this.Length);
|
||||
var actualMaxChars = @this.Length - safeOffset;
|
||||
|
||||
var safeMaxChars = maxChars < 0 ? actualMaxChars : Math.Min(maxChars, actualMaxChars);
|
||||
|
||||
return @this.Substring(safeOffset, safeMaxChars);
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
public static string Right([NotNull] this string @this, int maxChars)
|
||||
{
|
||||
var safeMaxChars = Math.Min(maxChars, @this.Length);
|
||||
return @this.Substring(@this.Length - safeMaxChars, safeMaxChars);
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
public static string StringJoin([NotNull] this IEnumerable @this, [NotNull] string separator)
|
||||
=> string.Join(separator, @this.Cast<object>());
|
||||
|
||||
[NotNull]
|
||||
public static string StringJoin([NotNull] this IEnumerable @this, char separator)
|
||||
=> string.Join(new string(separator, 1), @this.Cast<object>());
|
||||
|
||||
[NotNull]
|
||||
public static string StringJoin<T, TSelected>([NotNull] this IEnumerable<T> @this, [NotNull] Func<T, TSelected> selector, [NotNull] string separator)
|
||||
=> string.Join(separator, @this.Select(selector));
|
||||
|
||||
[NotNull]
|
||||
public static string StringJoin<T, TSelected>([NotNull] this IEnumerable<T> @this, [NotNull] Func<T, TSelected> selector, char separator)
|
||||
=> string.Join(new string(separator, 1), @this.Select(selector));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="../common.targets" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations">
|
||||
<Version>11.1.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.ValueTuple">
|
||||
<Version>4.4.0</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,5 +1,5 @@
|
|||
<Project>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);**\*.bak</DefaultItemExcludes>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);**\*.bak;**\*.orig</DefaultItemExcludes>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using NSubstitute.Elevated.Weaver;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace NSubstitute.Elevated.Tests
|
||||
{
|
||||
|
||||
[TestFixture]
|
||||
public class ElevatedWeaverTests
|
||||
{
|
||||
TestAssembly m_FixtureTestAssembly;
|
||||
|
||||
const string k_FixtureTestCode = @"
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ShouldNotPatch
|
||||
{
|
||||
interface Interface { void Foo(); } // ordinary proxying works
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
struct StructWithLayoutAttr { } // don't want to risk breaking things by changing size
|
||||
|
||||
class ClassWithPrivateNestedType
|
||||
{
|
||||
class PrivateNested { } // unavailable externally, no point
|
||||
}
|
||||
|
||||
class ClassWithGeneratedNestedType
|
||||
{
|
||||
public IEnumerable<int> Foo() // this causes a state machine type to be generated which shouldn't be patched
|
||||
{ yield return 1; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace ShouldPatch
|
||||
{
|
||||
class ClassWithNestedTypes
|
||||
{
|
||||
public class PublicNested { }
|
||||
internal class InternalNested { }
|
||||
}
|
||||
}
|
||||
|
||||
";
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void OneTimeSetUp()
|
||||
{
|
||||
m_FixtureTestAssembly = new TestAssembly(nameof(ElevatedWeaverTests), k_FixtureTestCode);
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public void OneTimeTearDown()
|
||||
{
|
||||
m_FixtureTestAssembly?.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Interfaces_ShouldNotPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.Interface");
|
||||
MockInjector.IsPatched(type).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PotentiallyBlittableStructs_ShouldNotPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.StructWithLayoutAttr");
|
||||
MockInjector.IsPatched(type).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PrivateNestedTypes_ShouldNotPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.ClassWithPrivateNestedType");
|
||||
var nestedType = type.NestedTypes.Single(t => t.Name == "PrivateNested");
|
||||
MockInjector.IsPatched(nestedType).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GeneratedTypes_ShouldNotPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldNotPatch.ClassWithGeneratedNestedType");
|
||||
type.NestedTypes.Count.ShouldBe(1); // this is the yield state machine, will be mangled name
|
||||
MockInjector.IsPatched(type.NestedTypes[0]).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TopLevelClass_ShouldPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes");
|
||||
MockInjector.IsPatched(type).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PublicNestedClasses_ShouldPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes");
|
||||
var nestedType = type.NestedTypes.Single(t => t.Name == "PublicNested");
|
||||
MockInjector.IsPatched(nestedType).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InternalNestedClasses_ShouldPatch()
|
||||
{
|
||||
var type = m_FixtureTestAssembly.GetType("ShouldPatch.ClassWithNestedTypes");
|
||||
var nestedType = type.NestedTypes.Single(t => t.Name == "InternalNested");
|
||||
MockInjector.IsPatched(nestedType).ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace NSubstitute.Elevated.Tests
|
||||
{
|
||||
public static class MockWeaverTestUtils
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
using System;
|
||||
using NUnit.Framework;
|
||||
|
||||
#pragma warning disable 169
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace NSubstitute.Elevated.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class MockWeaverTests
|
||||
{
|
||||
[NonSerialized]
|
||||
object __mockContext;
|
||||
[NonSerialized]
|
||||
static object __mockStaticContext;
|
||||
|
||||
[Test]
|
||||
public void NonParamStaticMethod()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,19 +8,16 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations">
|
||||
<Version>11.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Mono.Cecil">
|
||||
<Version>0.9.6.4</Version>
|
||||
<Version>11.1.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NSubstitute">
|
||||
<Version>2.0.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NUnit">
|
||||
<Version>3.5.0</Version>
|
||||
<Version>3.8.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly">
|
||||
<Version>2.8.2</Version>
|
||||
<Version>2.8.3</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.CodeDom.Compiler;
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
var results = ElevatedWeaver.PatchAllDependentAssemblies(m_TestAssemblyPath, PatchTestAssembly.Yes);
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче