From 5c913ba6154ae1305f8d8691bbee7d7fe9239df1 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 9 Feb 2021 09:22:26 -0600 Subject: [PATCH] Further changes around ScanLibrary. Refactored DirectoryService search pattern to allow for greater re-usability. Fixed a bug where leftover chapters and volumes wouldn't get cleaned up when removed from disk. --- API.Tests/Helpers/PrivateObjectPrivateType.cs | 1864 +++++++++++++++++ API.Tests/ParserTest.cs | 17 +- API.Tests/Services/DirectoryServiceTests.cs | 29 +- API.Tests/Services/ScannerServiceTests.cs | 57 +- API/Entities/Series.cs | 2 +- API/Services/DirectoryService.cs | 4 +- API/Services/ScannerService.cs | 36 +- 7 files changed, 1967 insertions(+), 42 deletions(-) create mode 100644 API.Tests/Helpers/PrivateObjectPrivateType.cs diff --git a/API.Tests/Helpers/PrivateObjectPrivateType.cs b/API.Tests/Helpers/PrivateObjectPrivateType.cs new file mode 100644 index 000000000..e99016828 --- /dev/null +++ b/API.Tests/Helpers/PrivateObjectPrivateType.cs @@ -0,0 +1,1864 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; + +namespace Microsoft.VisualStudio.TestTools.UnitTesting +{ + /// + /// This class represents the live NON public INTERNAL object in the system + /// + public class PrivateObject + { + // bind everything + private const BindingFlags BindToEveryThing = BindingFlags.Default | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public; + + private static BindingFlags constructorFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance | BindingFlags.NonPublic; + + private object target; // automatically initialized to null + private Type originalType; // automatically initialized to null + + private Dictionary> methodCache; // automatically initialized to null + + /// + /// Initializes a new instance of the class that contains + /// the already existing object of the private class + /// + /// object that serves as starting point to reach the private members + /// the derefrencing string using . that points to the object to be retrived as in m_X.m_Y.m_Z + public PrivateObject(object obj, string memberToAccess) + { + ValidateAccessString(memberToAccess); + + PrivateObject temp = obj as PrivateObject; + if (temp == null) + { + temp = new PrivateObject(obj); + } + + // Split The access string + string[] arr = memberToAccess.Split(new char[] { '.' }); + + for (int i = 0; i < arr.Length; i++) + { + object next = temp.InvokeHelper(arr[i], BindToEveryThing | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.GetProperty, null, CultureInfo.InvariantCulture); + temp = new PrivateObject(next); + } + + this.target = temp.target; + this.originalType = temp.originalType; + } + + /// + /// Initializes a new instance of the class that wraps the + /// specified type. + /// + /// Name of the assembly + /// fully qualified name + /// Argmenets to pass to the constructor + public PrivateObject(string assemblyName, string typeName, params object[] args) + : this(assemblyName, typeName, null, args) + { + } + + /// + /// Initializes a new instance of the class that wraps the + /// specified type. + /// + /// Name of the assembly + /// fully qualified name + /// An array of objects representing the number, order, and type of the parameters for the constructor to get + /// Argmenets to pass to the constructor + public PrivateObject(string assemblyName, string typeName, Type[] parameterTypes, object[] args) + : this(Type.GetType(string.Format(CultureInfo.InvariantCulture, "{0}, {1}", typeName, assemblyName), false), parameterTypes, args) + { + } + + /// + /// Initializes a new instance of the class that wraps the + /// specified type. + /// + /// type of the object to create + /// Argmenets to pass to the constructor + public PrivateObject(Type type, params object[] args) + : this(type, null, args) + { + } + + /// + /// Initializes a new instance of the class that wraps the + /// specified type. + /// + /// type of the object to create + /// An array of objects representing the number, order, and type of the parameters for the constructor to get + /// Argmenets to pass to the constructor + public PrivateObject(Type type, Type[] parameterTypes, object[] args) + { + object o; + if (parameterTypes != null) + { + ConstructorInfo ci = type.GetConstructor(BindToEveryThing, null, parameterTypes, null); + if (ci == null) + { + throw new ArgumentException("The constructor with the specified signature could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor."); + } + + try + { + o = ci.Invoke(args); + } + catch (TargetInvocationException e) + { + Debug.Assert(e.InnerException != null, "Inner exception should not be null."); + if (e.InnerException != null) + { + throw e.InnerException; + } + + throw; + } + } + else + { + o = Activator.CreateInstance(type, constructorFlags, null, args, null); + } + + this.ConstructFrom(o); + } + + /// + /// Initializes a new instance of the class that wraps + /// the given object. + /// + /// object to wrap + public PrivateObject(object obj) + { + this.ConstructFrom(obj); + } + + /// + /// Initializes a new instance of the class that wraps + /// the given object. + /// + /// object to wrap + /// PrivateType object + public PrivateObject(object obj, PrivateType type) + { + this.target = obj; + this.originalType = type.ReferencedType; + } + + /// + /// Gets or sets the target + /// + public object Target + { + get + { + return this.target; + } + + set + { + this.target = value; + this.originalType = value.GetType(); + } + } + + /// + /// Gets the type of underlying object + /// + public Type RealType + { + get + { + return this.originalType; + } + } + + private Dictionary> GenericMethodCache + { + get + { + if (this.methodCache == null) + { + this.BuildGenericMethodCacheForType(this.originalType); + } + + Debug.Assert(this.methodCache != null, "Invalid method cache for type."); + + return this.methodCache; + } + } + + /// + /// returns the hash code of the target object + /// + /// int representing hashcode of the target object + public override int GetHashCode() + { + Debug.Assert(this.target != null, "target should not be null."); + return this.target.GetHashCode(); + } + + /// + /// Equals + /// + /// Object with whom to compare + /// returns true if the objects are equal. + public override bool Equals(object obj) + { + if (this != obj) + { + Debug.Assert(this.target != null, "target should not be null."); + if (typeof(PrivateObject) == obj?.GetType()) + { + return this.target.Equals(((PrivateObject)obj).target); + } + else + { + return false; + } + } + + return true; + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// Arguments to pass to the member to invoke. + /// Result of method call + public object Invoke(string name, params object[] args) + { + return this.Invoke(name, null, args, CultureInfo.InvariantCulture); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// An array of objects representing the number, order, and type of the parameters for the method to get. + /// Arguments to pass to the member to invoke. + /// Result of method call + public object Invoke(string name, Type[] parameterTypes, object[] args) + { + return this.Invoke(name, parameterTypes, args, CultureInfo.InvariantCulture); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// An array of objects representing the number, order, and type of the parameters for the method to get. + /// Arguments to pass to the member to invoke. + /// An array of types corresponding to the types of the generic arguments. + /// Result of method call + public object Invoke(string name, Type[] parameterTypes, object[] args, Type[] typeArguments) + { + return this.Invoke(name, BindToEveryThing, parameterTypes, args, CultureInfo.InvariantCulture, typeArguments); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// Arguments to pass to the member to invoke. + /// Culture info + /// Result of method call + public object Invoke(string name, object[] args, CultureInfo culture) + { + return this.Invoke(name, null, args, culture); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// An array of objects representing the number, order, and type of the parameters for the method to get. + /// Arguments to pass to the member to invoke. + /// Culture info + /// Result of method call + public object Invoke(string name, Type[] parameterTypes, object[] args, CultureInfo culture) + { + return this.Invoke(name, BindToEveryThing, parameterTypes, args, culture); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// A bitmask comprised of one or more that specify how the search is conducted. + /// Arguments to pass to the member to invoke. + /// Result of method call + public object Invoke(string name, BindingFlags bindingFlags, params object[] args) + { + return this.Invoke(name, bindingFlags, null, args, CultureInfo.InvariantCulture); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// A bitmask comprised of one or more that specify how the search is conducted. + /// An array of objects representing the number, order, and type of the parameters for the method to get. + /// Arguments to pass to the member to invoke. + /// Result of method call + public object Invoke(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args) + { + return this.Invoke(name, bindingFlags, parameterTypes, args, CultureInfo.InvariantCulture); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// A bitmask comprised of one or more that specify how the search is conducted. + /// Arguments to pass to the member to invoke. + /// Culture info + /// Result of method call + public object Invoke(string name, BindingFlags bindingFlags, object[] args, CultureInfo culture) + { + return this.Invoke(name, bindingFlags, null, args, culture); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// A bitmask comprised of one or more that specify how the search is conducted. + /// An array of objects representing the number, order, and type of the parameters for the method to get. + /// Arguments to pass to the member to invoke. + /// Culture info + /// Result of method call + public object Invoke(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args, CultureInfo culture) + { + return this.Invoke(name, bindingFlags, parameterTypes, args, culture, null); + } + + /// + /// Invokes the specified method + /// + /// Name of the method + /// A bitmask comprised of one or more that specify how the search is conducted. + /// An array of objects representing the number, order, and type of the parameters for the method to get. + /// Arguments to pass to the member to invoke. + /// Culture info + /// An array of types corresponding to the types of the generic arguments. + /// Result of method call + public object Invoke(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args, CultureInfo culture, Type[] typeArguments) + { + if (parameterTypes != null) + { + bindingFlags |= BindToEveryThing | BindingFlags.Instance; + + // Fix up the parameter types + MethodInfo member = this.originalType.GetMethod(name, bindingFlags, null, parameterTypes, null); + + // If the method was not found and type arguments were provided for generic paramaters, + // attempt to look up a generic method. + if ((member == null) && (typeArguments != null)) + { + // This method may contain generic parameters...if so, the previous call to + // GetMethod() will fail because it doesn't fully support generic parameters. + + // Look in the method cache to see if there is a generic method + // on the incoming type that contains the correct signature. + member = this.GetGenericMethodFromCache(name, parameterTypes, typeArguments, bindingFlags, null); + } + + if (member == null) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); + } + + try + { + if (member.IsGenericMethodDefinition) + { + MethodInfo constructed = member.MakeGenericMethod(typeArguments); + return constructed.Invoke(this.target, bindingFlags, null, args, culture); + } + else + { + return member.Invoke(this.target, bindingFlags, null, args, culture); + } + } + catch (TargetInvocationException e) + { + Debug.Assert(e.InnerException != null, "Inner exception should not be null."); + if (e.InnerException != null) + { + throw e.InnerException; + } + + throw; + } + } + else + { + return this.InvokeHelper(name, bindingFlags | BindingFlags.InvokeMethod, args, culture); + } + } + + /// + /// Gets the array element using array of subsrcipts for each dimension + /// + /// Name of the member + /// the indices of array + /// An arrya of elements. + public object GetArrayElement(string name, params int[] indices) + { + return this.GetArrayElement(name, BindToEveryThing, indices); + } + + /// + /// Sets the array element using array of subsrcipts for each dimension + /// + /// Name of the member + /// Value to set + /// the indices of array + public void SetArrayElement(string name, object value, params int[] indices) + { + this.SetArrayElement(name, BindToEveryThing, value, indices); + } + + /// + /// Gets the array element using array of subsrcipts for each dimension + /// + /// Name of the member + /// A bitmask comprised of one or more that specify how the search is conducted. + /// the indices of array + /// An arrya of elements. + public object GetArrayElement(string name, BindingFlags bindingFlags, params int[] indices) + { + Array arr = (Array)this.InvokeHelper(name, BindingFlags.GetField | bindingFlags, null, CultureInfo.InvariantCulture); + return arr.GetValue(indices); + } + + /// + /// Sets the array element using array of subsrcipts for each dimension + /// + /// Name of the member + /// A bitmask comprised of one or more that specify how the search is conducted. + /// Value to set + /// the indices of array + public void SetArrayElement(string name, BindingFlags bindingFlags, object value, params int[] indices) + { + Array arr = (Array)this.InvokeHelper(name, BindingFlags.GetField | bindingFlags, null, CultureInfo.InvariantCulture); + arr.SetValue(value, indices); + } + + /// + /// Get the field + /// + /// Name of the field + /// The field. + public object GetField(string name) + { + return this.GetField(name, BindToEveryThing); + } + + /// + /// Sets the field + /// + /// Name of the field + /// value to set + public void SetField(string name, object value) + { + this.SetField(name, BindToEveryThing, value); + } + + /// + /// Gets the field + /// + /// Name of the field + /// A bitmask comprised of one or more that specify how the search is conducted. + /// The field. + public object GetField(string name, BindingFlags bindingFlags) + { + return this.InvokeHelper(name, BindingFlags.GetField | bindingFlags, null, CultureInfo.InvariantCulture); + } + + /// + /// Sets the field + /// + /// Name of the field + /// A bitmask comprised of one or more that specify how the search is conducted. + /// value to set + public void SetField(string name, BindingFlags bindingFlags, object value) + { + this.InvokeHelper(name, BindingFlags.SetField | bindingFlags, new object[] { value }, CultureInfo.InvariantCulture); + } + + /// + /// Get the field or property + /// + /// Name of the field or property + /// The field or property. + public object GetFieldOrProperty(string name) + { + return this.GetFieldOrProperty(name, BindToEveryThing); + } + + /// + /// Sets the field or property + /// + /// Name of the field or property + /// value to set + public void SetFieldOrProperty(string name, object value) + { + this.SetFieldOrProperty(name, BindToEveryThing, value); + } + + /// + /// Gets the field or property + /// + /// Name of the field or property + /// A bitmask comprised of one or more that specify how the search is conducted. + /// The field or property. + public object GetFieldOrProperty(string name, BindingFlags bindingFlags) + { + return this.InvokeHelper(name, BindingFlags.GetField | BindingFlags.GetProperty | bindingFlags, null, CultureInfo.InvariantCulture); + } + + /// + /// Sets the field or property + /// + /// Name of the field or property + /// A bitmask comprised of one or more that specify how the search is conducted. + /// value to set + public void SetFieldOrProperty(string name, BindingFlags bindingFlags, object value) + { + this.InvokeHelper(name, BindingFlags.SetField | BindingFlags.SetProperty | bindingFlags, new object[] { value }, CultureInfo.InvariantCulture); + } + + /// + /// Gets the property + /// + /// Name of the property + /// Arguments to pass to the member to invoke. + /// The property. + public object GetProperty(string name, params object[] args) + { + return this.GetProperty(name, null, args); + } + + /// + /// Gets the property + /// + /// Name of the property + /// An array of objects representing the number, order, and type of the parameters for the indexed property. + /// Arguments to pass to the member to invoke. + /// The property. + public object GetProperty(string name, Type[] parameterTypes, object[] args) + { + return this.GetProperty(name, BindToEveryThing, parameterTypes, args); + } + + /// + /// Set the property + /// + /// Name of the property + /// value to set + /// Arguments to pass to the member to invoke. + public void SetProperty(string name, object value, params object[] args) + { + this.SetProperty(name, null, value, args); + } + + /// + /// Set the property + /// + /// Name of the property + /// An array of objects representing the number, order, and type of the parameters for the indexed property. + /// value to set + /// Arguments to pass to the member to invoke. + public void SetProperty(string name, Type[] parameterTypes, object value, object[] args) + { + this.SetProperty(name, BindToEveryThing, value, parameterTypes, args); + } + + /// + /// Gets the property + /// + /// Name of the property + /// A bitmask comprised of one or more that specify how the search is conducted. + /// Arguments to pass to the member to invoke. + /// The property. + public object GetProperty(string name, BindingFlags bindingFlags, params object[] args) + { + return this.GetProperty(name, bindingFlags, null, args); + } + + /// + /// Gets the property + /// + /// Name of the property + /// A bitmask comprised of one or more that specify how the search is conducted. + /// An array of objects representing the number, order, and type of the parameters for the indexed property. + /// Arguments to pass to the member to invoke. + /// The property. + public object GetProperty(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args) + { + if (parameterTypes != null) + { + PropertyInfo pi = this.originalType.GetProperty(name, bindingFlags, null, null, parameterTypes, null); + if (pi == null) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); + } + + return pi.GetValue(this.target, args); + } + else + { + return this.InvokeHelper(name, bindingFlags | BindingFlags.GetProperty, args, null); + } + } + + /// + /// Sets the property + /// + /// Name of the property + /// A bitmask comprised of one or more that specify how the search is conducted. + /// value to set + /// Arguments to pass to the member to invoke. + public void SetProperty(string name, BindingFlags bindingFlags, object value, params object[] args) + { + this.SetProperty(name, bindingFlags, value, null, args); + } + + /// + /// Sets the property + /// + /// Name of the property + /// A bitmask comprised of one or more that specify how the search is conducted. + /// value to set + /// An array of objects representing the number, order, and type of the parameters for the indexed property. + /// Arguments to pass to the member to invoke. + public void SetProperty(string name, BindingFlags bindingFlags, object value, Type[] parameterTypes, object[] args) + { + if (parameterTypes != null) + { + PropertyInfo pi = this.originalType.GetProperty(name, bindingFlags, null, null, parameterTypes, null); + if (pi == null) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); + } + + pi.SetValue(this.target, value, args); + } + else + { + object[] pass = new object[(args?.Length ?? 0) + 1]; + pass[0] = value; + args?.CopyTo(pass, 1); + this.InvokeHelper(name, bindingFlags | BindingFlags.SetProperty, pass, null); + } + } + + /// + /// Validate access string + /// + /// access string + private static void ValidateAccessString(string access) + { + if (access.Length == 0) + { + throw new ArgumentException("Access string has invalid syntax."); + } + + string[] arr = access.Split('.'); + foreach (string str in arr) + { + if ((str.Length == 0) || (str.IndexOfAny(new char[] { ' ', '\t', '\n' }) != -1)) + { + throw new ArgumentException("Access string has invalid syntax."); + } + } + } + + /// + /// Invokes the memeber + /// + /// Name of the member + /// Additional attributes + /// Arguments for the invocation + /// Culture + /// Result of the invocation + private object InvokeHelper(string name, BindingFlags bindingFlags, object[] args, CultureInfo culture) + { + Debug.Assert(this.target != null, "Internal Error: Null reference is returned for internal object"); + + // Invoke the actual Method + try + { + return this.originalType.InvokeMember(name, bindingFlags, null, this.target, args, culture); + } + catch (TargetInvocationException e) + { + Debug.Assert(e.InnerException != null, "Inner exception should not be null."); + if (e.InnerException != null) + { + throw e.InnerException; + } + + throw; + } + } + + private void ConstructFrom(object obj) + { + this.target = obj; + this.originalType = obj.GetType(); + } + + private void BuildGenericMethodCacheForType(Type t) + { + Debug.Assert(t != null, "type should not be null."); + this.methodCache = new Dictionary>(); + + MethodInfo[] members = t.GetMethods(BindToEveryThing); + LinkedList listByName; // automatically initialized to null + + foreach (MethodInfo member in members) + { + if (member.IsGenericMethod || member.IsGenericMethodDefinition) + { + if (!this.GenericMethodCache.TryGetValue(member.Name, out listByName)) + { + listByName = new LinkedList(); + this.GenericMethodCache.Add(member.Name, listByName); + } + + Debug.Assert(listByName != null, "list should not be null."); + listByName.AddLast(member); + } + } + } + + /// + /// Extracts the most appropriate generic method signature from the current private type. + /// + /// The name of the method in which to search the signature cache. + /// An array of types corresponding to the types of the parameters in which to search. + /// An array of types corresponding to the types of the generic arguments. + /// to further filter the method signatures. + /// Modifiers for parameters. + /// A methodinfo instance. + private MethodInfo GetGenericMethodFromCache(string methodName, Type[] parameterTypes, Type[] typeArguments, BindingFlags bindingFlags, ParameterModifier[] modifiers) + { + Debug.Assert(!string.IsNullOrEmpty(methodName), "Invalid method name."); + Debug.Assert(parameterTypes != null, "Invalid parameter type array."); + Debug.Assert(typeArguments != null, "Invalid type arguments array."); + + // Build a preliminary list of method candidates that contain roughly the same signature. + var methodCandidates = this.GetMethodCandidates(methodName, parameterTypes, typeArguments, bindingFlags, modifiers); + + // Search of ambiguous methods (methods with the same signature). + MethodInfo[] finalCandidates = new MethodInfo[methodCandidates.Count]; + methodCandidates.CopyTo(finalCandidates, 0); + + if ((parameterTypes != null) && (parameterTypes.Length == 0)) + { + for (int i = 0; i < finalCandidates.Length; i++) + { + MethodInfo methodInfo = finalCandidates[i]; + + if (!RuntimeTypeHelper.CompareMethodSigAndName(methodInfo, finalCandidates[0])) + { + throw new AmbiguousMatchException(); + } + } + + // All the methods have the exact same name and sig so return the most derived one. + return RuntimeTypeHelper.FindMostDerivedNewSlotMeth(finalCandidates, finalCandidates.Length) as MethodInfo; + } + + // Now that we have a preliminary list of candidates, select the most appropriate one. + return RuntimeTypeHelper.SelectMethod(bindingFlags, finalCandidates, parameterTypes, modifiers) as MethodInfo; + } + + private LinkedList GetMethodCandidates(string methodName, Type[] parameterTypes, Type[] typeArguments, BindingFlags bindingFlags, ParameterModifier[] modifiers) + { + Debug.Assert(!string.IsNullOrEmpty(methodName), "methodName should not be null."); + Debug.Assert(parameterTypes != null, "parameterTypes should not be null."); + Debug.Assert(typeArguments != null, "typeArguments should not be null."); + + LinkedList methodCandidates = new LinkedList(); + LinkedList methods = null; + + if (!this.GenericMethodCache.TryGetValue(methodName, out methods)) + { + return methodCandidates; + } + + Debug.Assert(methods != null, "methods should not be null."); + + foreach (MethodInfo candidate in methods) + { + bool paramMatch = true; + ParameterInfo[] candidateParams = null; + Type[] genericArgs = candidate.GetGenericArguments(); + Type sourceParameterType = null; + + if (genericArgs.Length != typeArguments.Length) + { + continue; + } + + // Since we can't just get the correct MethodInfo from Reflection, + // we will just match the number of parameters, their order, and their type + var methodCandidate = candidate; + candidateParams = methodCandidate.GetParameters(); + + if (candidateParams.Length != parameterTypes.Length) + { + continue; + } + + // Exact binding + if ((bindingFlags & BindingFlags.ExactBinding) != 0) + { + int i = 0; + + foreach (ParameterInfo candidateParam in candidateParams) + { + sourceParameterType = parameterTypes[i++]; + + if (candidateParam.ParameterType.ContainsGenericParameters) + { + // Since we have a generic parameter here, just make sure the IsArray matches. + if (candidateParam.ParameterType.IsArray != sourceParameterType.IsArray) + { + paramMatch = false; + break; + } + } + else + { + if (candidateParam.ParameterType != sourceParameterType) + { + paramMatch = false; + break; + } + } + } + + if (paramMatch) + { + methodCandidates.AddLast(methodCandidate); + continue; + } + } + else + { + methodCandidates.AddLast(methodCandidate); + } + } + + return methodCandidates; + } + } + + /// + /// This class represents a private class for the Private Accessor functionality. + /// + public class PrivateType + { + /// + /// Binds to everything + /// + private const BindingFlags BindToEveryThing = BindingFlags.Default + | BindingFlags.NonPublic | BindingFlags.Instance + | BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy; + + /// + /// The wrapped type. + /// + private Type type; + + /// + /// Initializes a new instance of the class that contains the private type. + /// + /// Assembly name + /// fully qualified name of the + public PrivateType(string assemblyName, string typeName) + { + Assembly asm = Assembly.Load(assemblyName); + + this.type = asm.GetType(typeName, true); + } + + /// + /// Initializes a new instance of the class that contains + /// the private type from the type object + /// + /// The wrapped Type to create. + public PrivateType(Type type) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + + this.type = type; + } + + /// + /// Gets the referenced type + /// + public Type ReferencedType => this.type; + + /// + /// Invokes static member + /// + /// Name of the member to InvokeHelper + /// Arguements to the invoction + /// Result of invocation + public object InvokeStatic(string name, params object[] args) + { + return this.InvokeStatic(name, null, args, CultureInfo.InvariantCulture); + } + + /// + /// Invokes static member + /// + /// Name of the member to InvokeHelper + /// An array of objects representing the number, order, and type of the parameters for the method to invoke + /// Arguements to the invoction + /// Result of invocation + public object InvokeStatic(string name, Type[] parameterTypes, object[] args) + { + return this.InvokeStatic(name, parameterTypes, args, CultureInfo.InvariantCulture); + } + + /// + /// Invokes static member + /// + /// Name of the member to InvokeHelper + /// An array of objects representing the number, order, and type of the parameters for the method to invoke + /// Arguements to the invoction + /// An array of types corresponding to the types of the generic arguments. + /// Result of invocation + public object InvokeStatic(string name, Type[] parameterTypes, object[] args, Type[] typeArguments) + { + return this.InvokeStatic(name, BindToEveryThing, parameterTypes, args, CultureInfo.InvariantCulture, typeArguments); + } + + /// + /// Invokes the static method + /// + /// Name of the member + /// Arguements to the invocation + /// Culture + /// Result of invocation + public object InvokeStatic(string name, object[] args, CultureInfo culture) + { + return this.InvokeStatic(name, null, args, culture); + } + + /// + /// Invokes the static method + /// + /// Name of the member + /// An array of objects representing the number, order, and type of the parameters for the method to invoke + /// Arguements to the invocation + /// Culture info + /// Result of invocation + public object InvokeStatic(string name, Type[] parameterTypes, object[] args, CultureInfo culture) + { + return this.InvokeStatic(name, BindingFlags.InvokeMethod, parameterTypes, args, culture); + } + + /// + /// Invokes the static method + /// + /// Name of the member + /// Additional invocation attributes + /// Arguements to the invocation + /// Result of invocation + public object InvokeStatic(string name, BindingFlags bindingFlags, params object[] args) + { + return this.InvokeStatic(name, bindingFlags, null, args, CultureInfo.InvariantCulture); + } + + /// + /// Invokes the static method + /// + /// Name of the member + /// Additional invocation attributes + /// An array of objects representing the number, order, and type of the parameters for the method to invoke + /// Arguements to the invocation + /// Result of invocation + public object InvokeStatic(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args) + { + return this.InvokeStatic(name, bindingFlags, parameterTypes, args, CultureInfo.InvariantCulture); + } + + /// + /// Invokes the static method + /// + /// Name of the member + /// Additional invocation attributes + /// Arguements to the invocation + /// Culture + /// Result of invocation + public object InvokeStatic(string name, BindingFlags bindingFlags, object[] args, CultureInfo culture) + { + return this.InvokeStatic(name, bindingFlags, null, args, culture); + } + + /// + /// Invokes the static method + /// + /// Name of the member + /// Additional invocation attributes + /// /// An array of objects representing the number, order, and type of the parameters for the method to invoke + /// Arguements to the invocation + /// Culture + /// Result of invocation + public object InvokeStatic(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args, CultureInfo culture) + { + return this.InvokeStatic(name, bindingFlags, parameterTypes, args, culture, null); + } + + /// + /// Invokes the static method + /// + /// Name of the member + /// Additional invocation attributes + /// /// An array of objects representing the number, order, and type of the parameters for the method to invoke + /// Arguements to the invocation + /// Culture + /// An array of types corresponding to the types of the generic arguments. + /// Result of invocation + public object InvokeStatic(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args, CultureInfo culture, Type[] typeArguments) + { + if (parameterTypes != null) + { + MethodInfo member = this.type.GetMethod(name, bindingFlags | BindToEveryThing | BindingFlags.Static, null, parameterTypes, null); + if (member == null) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); + } + + try + { + if (member.IsGenericMethodDefinition) + { + MethodInfo constructed = member.MakeGenericMethod(typeArguments); + return constructed.Invoke(null, bindingFlags, null, args, culture); + } + else + { + return member.Invoke(null, bindingFlags, null, args, culture); + } + } + catch (TargetInvocationException e) + { + Debug.Assert(e.InnerException != null, "Inner Exception should not be null."); + if (e.InnerException != null) + { + throw e.InnerException; + } + + throw; + } + } + else + { + return this.InvokeHelperStatic(name, bindingFlags | BindingFlags.InvokeMethod, args, culture); + } + } + + /// + /// Gets the element in static array + /// + /// Name of the array + /// + /// A one-dimensional array of 32-bit integers that represent the indexes specifying + /// the position of the element to get. For instance, to access a[10][11] the indices would be {10,11} + /// + /// element at the specified location + public object GetStaticArrayElement(string name, params int[] indices) + { + return this.GetStaticArrayElement(name, BindToEveryThing, indices); + } + + /// + /// Sets the memeber of the static array + /// + /// Name of the array + /// value to set + /// + /// A one-dimensional array of 32-bit integers that represent the indexes specifying + /// the position of the element to set. For instance, to access a[10][11] the array would be {10,11} + /// + public void SetStaticArrayElement(string name, object value, params int[] indices) + { + this.SetStaticArrayElement(name, BindToEveryThing, value, indices); + } + + /// + /// Gets the element in satatic array + /// + /// Name of the array + /// Additional InvokeHelper attributes + /// + /// A one-dimensional array of 32-bit integers that represent the indexes specifying + /// the position of the element to get. For instance, to access a[10][11] the array would be {10,11} + /// + /// element at the spcified location + public object GetStaticArrayElement(string name, BindingFlags bindingFlags, params int[] indices) + { + Array arr = (Array)this.InvokeHelperStatic(name, BindingFlags.GetField | BindingFlags.GetProperty | bindingFlags, null, CultureInfo.InvariantCulture); + return arr.GetValue(indices); + } + + /// + /// Sets the memeber of the static array + /// + /// Name of the array + /// Additional InvokeHelper attributes + /// value to set + /// + /// A one-dimensional array of 32-bit integers that represent the indexes specifying + /// the position of the element to set. For instance, to access a[10][11] the array would be {10,11} + /// + public void SetStaticArrayElement(string name, BindingFlags bindingFlags, object value, params int[] indices) + { + Array arr = (Array)this.InvokeHelperStatic(name, BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Static | bindingFlags, null, CultureInfo.InvariantCulture); + arr.SetValue(value, indices); + } + + /// + /// Gets the static field + /// + /// Name of the field + /// The static field. + public object GetStaticField(string name) + { + return this.GetStaticField(name, BindToEveryThing); + } + + /// + /// Sets the static field + /// + /// Name of the field + /// Arguement to the invocation + public void SetStaticField(string name, object value) + { + this.SetStaticField(name, BindToEveryThing, value); + } + + /// + /// Gets the static field using specified InvokeHelper attributes + /// + /// Name of the field + /// Additional invocation attributes + /// The static field. + public object GetStaticField(string name, BindingFlags bindingFlags) + { + return this.InvokeHelperStatic(name, BindingFlags.GetField | BindingFlags.Static | bindingFlags, null, CultureInfo.InvariantCulture); + } + + /// + /// Sets the static field using binding attributes + /// + /// Name of the field + /// Additional InvokeHelper attributes + /// Arguement to the invocation + public void SetStaticField(string name, BindingFlags bindingFlags, object value) + { + this.InvokeHelperStatic(name, BindingFlags.SetField | bindingFlags | BindingFlags.Static, new[] { value }, CultureInfo.InvariantCulture); + } + + /// + /// Gets the static field or property + /// + /// Name of the field or property + /// The static field or property. + public object GetStaticFieldOrProperty(string name) + { + return this.GetStaticFieldOrProperty(name, BindToEveryThing); + } + + /// + /// Sets the static field or property + /// + /// Name of the field or property + /// Value to be set to field or property + public void SetStaticFieldOrProperty(string name, object value) + { + this.SetStaticFieldOrProperty(name, BindToEveryThing, value); + } + + /// + /// Gets the static field or property using specified InvokeHelper attributes + /// + /// Name of the field or property + /// Additional invocation attributes + /// The static field or property. + public object GetStaticFieldOrProperty(string name, BindingFlags bindingFlags) + { + return this.InvokeHelperStatic(name, BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Static | bindingFlags, null, CultureInfo.InvariantCulture); + } + + /// + /// Sets the static field or property using binding attributes + /// + /// Name of the field or property + /// Additional invocation attributes + /// Value to be set to field or property + public void SetStaticFieldOrProperty(string name, BindingFlags bindingFlags, object value) + { + this.InvokeHelperStatic(name, BindingFlags.SetField | BindingFlags.SetProperty | bindingFlags | BindingFlags.Static, new[] { value }, CultureInfo.InvariantCulture); + } + + /// + /// Gets the static property + /// + /// Name of the field or property + /// Arguements to the invocation + /// The static property. + public object GetStaticProperty(string name, params object[] args) + { + return this.GetStaticProperty(name, BindToEveryThing, args); + } + + /// + /// Sets the static property + /// + /// Name of the property + /// Value to be set to field or property + /// Arguments to pass to the member to invoke. + public void SetStaticProperty(string name, object value, params object[] args) + { + this.SetStaticProperty(name, BindToEveryThing, value, null, args); + } + + /// + /// Sets the static property + /// + /// Name of the property + /// Value to be set to field or property + /// An array of objects representing the number, order, and type of the parameters for the indexed property. + /// Arguments to pass to the member to invoke. + public void SetStaticProperty(string name, object value, Type[] parameterTypes, object[] args) + { + this.SetStaticProperty(name, BindingFlags.SetProperty, value, parameterTypes, args); + } + + /// + /// Gets the static property + /// + /// Name of the property + /// Additional invocation attributes. + /// Arguments to pass to the member to invoke. + /// The static property. + public object GetStaticProperty(string name, BindingFlags bindingFlags, params object[] args) + { + return this.GetStaticProperty(name, BindingFlags.GetProperty | BindingFlags.Static | bindingFlags, null, args); + } + + /// + /// Gets the static property + /// + /// Name of the property + /// Additional invocation attributes. + /// An array of objects representing the number, order, and type of the parameters for the indexed property. + /// Arguments to pass to the member to invoke. + /// The static property. + public object GetStaticProperty(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args) + { + if (parameterTypes != null) + { + PropertyInfo pi = this.type.GetProperty(name, bindingFlags | BindingFlags.Static, null, null, parameterTypes, null); + if (pi == null) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); + } + + return pi.GetValue(null, args); + } + else + { + return this.InvokeHelperStatic(name, bindingFlags | BindingFlags.GetProperty, args, null); + } + } + + /// + /// Sets the static property + /// + /// Name of the property + /// Additional invocation attributes. + /// Value to be set to field or property + /// Optional index values for indexed properties. The indexes of indexed properties are zero-based. This value should be null for non-indexed properties. + public void SetStaticProperty(string name, BindingFlags bindingFlags, object value, params object[] args) + { + this.SetStaticProperty(name, bindingFlags, value, null, args); + } + + /// + /// Sets the static property + /// + /// Name of the property + /// Additional invocation attributes. + /// Value to be set to field or property + /// An array of objects representing the number, order, and type of the parameters for the indexed property. + /// Arguments to pass to the member to invoke. + public void SetStaticProperty(string name, BindingFlags bindingFlags, object value, Type[] parameterTypes, object[] args) + { + if (parameterTypes != null) + { + PropertyInfo pi = this.type.GetProperty(name, bindingFlags | BindingFlags.Static, null, null, parameterTypes, null); + if (pi == null) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); + } + + pi.SetValue(null, value, args); + } + else + { + object[] pass = new object[(args?.Length ?? 0) + 1]; + pass[0] = value; + args?.CopyTo(pass, 1); + this.InvokeHelperStatic(name, bindingFlags | BindingFlags.SetProperty, pass, null); + } + } + + /// + /// Invokes the static method + /// + /// Name of the member + /// Additional invocation attributes + /// Arguements to the invocation + /// Culture + /// Result of invocation + private object InvokeHelperStatic(string name, BindingFlags bindingFlags, object[] args, CultureInfo culture) + { + try + { + return this.type.InvokeMember(name, bindingFlags | BindToEveryThing | BindingFlags.Static, null, null, args, culture); + } + catch (TargetInvocationException e) + { + Debug.Assert(e.InnerException != null, "Inner Exception should not be null."); + if (e.InnerException != null) + { + throw e.InnerException; + } + + throw; + } + } + } + + /// + /// Provides method signature discovery for generic methods. + /// + internal class RuntimeTypeHelper + { + /// + /// Compares the method signatures of these two methods. + /// + /// Method1 + /// Method2 + /// True if they are similiar. + internal static bool CompareMethodSigAndName(MethodBase m1, MethodBase m2) + { + ParameterInfo[] params1 = m1.GetParameters(); + ParameterInfo[] params2 = m2.GetParameters(); + + if (params1.Length != params2.Length) + { + return false; + } + + int numParams = params1.Length; + for (int i = 0; i < numParams; i++) + { + if (params1[i].ParameterType != params2[i].ParameterType) + { + return false; + } + } + + return true; + } + + /// + /// Gets the hierarchy depth from the base type of the provided type. + /// + /// The type. + /// The depth. + internal static int GetHierarchyDepth(Type t) + { + int depth = 0; + + Type currentType = t; + do + { + depth++; + currentType = currentType.BaseType; + } + while (currentType != null); + + return depth; + } + + /// + /// Finds most dervied type with the provided information. + /// + /// Candidate matches. + /// Number of matches. + /// The most derived method. + internal static MethodBase FindMostDerivedNewSlotMeth(MethodBase[] match, int cMatches) + { + int deepestHierarchy = 0; + MethodBase methWithDeepestHierarchy = null; + + for (int i = 0; i < cMatches; i++) + { + // Calculate the depth of the hierarchy of the declaring type of the + // current method. + int currentHierarchyDepth = GetHierarchyDepth(match[i].DeclaringType); + + // Two methods with the same hierarchy depth are not allowed. This would + // mean that there are 2 methods with the same name and sig on a given type + // which is not allowed, unless one of them is vararg... + if (currentHierarchyDepth == deepestHierarchy) + { + if (methWithDeepestHierarchy != null) + { + Debug.Assert( + methWithDeepestHierarchy != null && ((match[i].CallingConvention & CallingConventions.VarArgs) + | (methWithDeepestHierarchy.CallingConvention & CallingConventions.VarArgs)) != 0, + "Calling conventions: " + match[i].CallingConvention + " - " + methWithDeepestHierarchy.CallingConvention); + } + + throw new AmbiguousMatchException(); + } + + // Check to see if this method is on the most derived class. + if (currentHierarchyDepth > deepestHierarchy) + { + deepestHierarchy = currentHierarchyDepth; + methWithDeepestHierarchy = match[i]; + } + } + + return methWithDeepestHierarchy; + } + + /// + /// Given a set of methods that match the base criteria, select a method based + /// upon an array of types. This method should return null if no method matches + /// the criteria. + /// + /// Binding specification. + /// Candidate matches + /// Types + /// Parameter modifiers. + /// Matching method. Null if none matches. + internal static MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + + int i; + int j; + + Type[] realTypes = new Type[types.Length]; + for (i = 0; i < types.Length; i++) + { + realTypes[i] = types[i].UnderlyingSystemType; + } + + types = realTypes; + + // If there are no methods to match to, then return null, indicating that no method + // matches the criteria + if (match.Length == 0) + { + return null; + } + + // Find all the methods that can be described by the types parameter. + // Remove all of them that cannot. + int curIdx = 0; + for (i = 0; i < match.Length; i++) + { + ParameterInfo[] par = match[i].GetParameters(); + if (par.Length != types.Length) + { + continue; + } + + for (j = 0; j < types.Length; j++) + { + Type pCls = par[j].ParameterType; + + if (pCls.ContainsGenericParameters) + { + if (pCls.IsArray != types[j].IsArray) + { + break; + } + } + else + { + if (pCls == types[j]) + { + continue; + } + + if (pCls == typeof(object)) + { + continue; + } + else + { + if (!pCls.IsAssignableFrom(types[j])) + { + break; + } + } + } + } + + if (j == types.Length) + { + match[curIdx++] = match[i]; + } + } + + if (curIdx == 0) + { + return null; + } + + if (curIdx == 1) + { + return match[0]; + } + + // Walk all of the methods looking the most specific method to invoke + int currentMin = 0; + bool ambig = false; + int[] paramOrder = new int[types.Length]; + for (i = 0; i < types.Length; i++) + { + paramOrder[i] = i; + } + + for (i = 1; i < curIdx; i++) + { + int newMin = FindMostSpecificMethod(match[currentMin], paramOrder, null, match[i], paramOrder, null, types, null); + if (newMin == 0) + { + ambig = true; + } + else + { + if (newMin == 2) + { + currentMin = i; + ambig = false; + currentMin = i; + } + } + } + + if (ambig) + { + throw new AmbiguousMatchException(); + } + + return match[currentMin]; + } + + /// + /// Finds the most specific method in the two methods provided. + /// + /// Method 1 + /// Parameter order for Method 1 + /// Paramter array type. + /// Method 2 + /// Parameter order for Method 2 + /// >Paramter array type. + /// Types to search in. + /// Args. + /// An int representing the match. + internal static int FindMostSpecificMethod( + MethodBase m1, + int[] paramOrder1, + Type paramArrayType1, + MethodBase m2, + int[] paramOrder2, + Type paramArrayType2, + Type[] types, + object[] args) + { + // Find the most specific method based on the parameters. + int res = FindMostSpecific( + m1.GetParameters(), + paramOrder1, + paramArrayType1, + m2.GetParameters(), + paramOrder2, + paramArrayType2, + types, + args); + + // If the match was not ambiguous then return the result. + if (res != 0) + { + return res; + } + + // Check to see if the methods have the exact same name and signature. + if (CompareMethodSigAndName(m1, m2)) + { + // Determine the depth of the declaring types for both methods. + int hierarchyDepth1 = GetHierarchyDepth(m1.DeclaringType); + int hierarchyDepth2 = GetHierarchyDepth(m2.DeclaringType); + + // The most derived method is the most specific one. + if (hierarchyDepth1 == hierarchyDepth2) + { + return 0; + } + else if (hierarchyDepth1 < hierarchyDepth2) + { + return 2; + } + else + { + return 1; + } + } + + // The match is ambiguous. + return 0; + } + + /// + /// Finds the most specific method in the two methods provided. + /// + /// Method 1 + /// Parameter order for Method 1 + /// Paramter array type. + /// Method 2 + /// Parameter order for Method 2 + /// >Paramter array type. + /// Types to search in. + /// Args. + /// An int representing the match. + internal static int FindMostSpecific( + ParameterInfo[] p1, + int[] paramOrder1, + Type paramArrayType1, + ParameterInfo[] p2, + int[] paramOrder2, + Type paramArrayType2, + Type[] types, + object[] args) + { + // A method using params is always less specific than one not using params + if (paramArrayType1 != null && paramArrayType2 == null) + { + return 2; + } + + if (paramArrayType2 != null && paramArrayType1 == null) + { + return 1; + } + + bool p1Less = false; + bool p2Less = false; + + for (int i = 0; i < types.Length; i++) + { + if (args != null && args[i] == Type.Missing) + { + continue; + } + + Type c1, c2; + + // If a param array is present, then either + // the user re-ordered the parameters in which case + // the argument to the param array is either an array + // in which case the params is conceptually ignored and so paramArrayType1 == null + // or the argument to the param array is a single element + // in which case paramOrder[i] == p1.Length - 1 for that element + // or the user did not re-order the parameters in which case + // the paramOrder array could contain indexes larger than p.Length - 1 + //// so any index >= p.Length - 1 is being put in the param array + + if (paramArrayType1 != null && paramOrder1[i] >= p1.Length - 1) + { + c1 = paramArrayType1; + } + else + { + c1 = p1[paramOrder1[i]].ParameterType; + } + + if (paramArrayType2 != null && paramOrder2[i] >= p2.Length - 1) + { + c2 = paramArrayType2; + } + else + { + c2 = p2[paramOrder2[i]].ParameterType; + } + + if (c1 == c2) + { + continue; + } + + if (c1.ContainsGenericParameters || c2.ContainsGenericParameters) + { + continue; + } + + switch (FindMostSpecificType(c1, c2, types[i])) + { + case 0: + return 0; + case 1: + p1Less = true; + break; + case 2: + p2Less = true; + break; + } + } + + // Two way p1Less and p2Less can be equal. All the arguments are the + // same they both equal false, otherwise there were things that both + // were the most specific type on.... + if (p1Less == p2Less) + { + // it's possible that the 2 methods have same sig and default param in which case we match the one + // with the same number of args but only if they were exactly the same (that is p1Less and p2Lees are both false) + if (!p1Less && p1.Length != p2.Length && args != null) + { + if (p1.Length == args.Length) + { + return 1; + } + else if (p2.Length == args.Length) + { + return 2; + } + } + + return 0; + } + else + { + return (p1Less == true) ? 1 : 2; + } + } + + /// + /// Finds the most specific type in the two provided. + /// + /// Type 1 + /// Type 2 + /// The defining type + /// An int representing the match. + internal static int FindMostSpecificType(Type c1, Type c2, Type t) + { + // If the two types are exact move on... + if (c1 == c2) + { + return 0; + } + + if (c1 == t) + { + return 1; + } + + if (c2 == t) + { + return 2; + } + + bool c1FromC2; + bool c2FromC1; + + if (c1.IsByRef || c2.IsByRef) + { + if (c1.IsByRef && c2.IsByRef) + { + c1 = c1.GetElementType(); + c2 = c2.GetElementType(); + } + else if (c1.IsByRef) + { + if (c1.GetElementType() == c2) + { + return 2; + } + + c1 = c1.GetElementType(); + } + else + { + if (c2.GetElementType() == c1) + { + return 1; + } + + c2 = c2.GetElementType(); + } + } + + if (c1.IsPrimitive && c2.IsPrimitive) + { + c1FromC2 = true; + c2FromC1 = true; + } + else + { + c1FromC2 = c1.IsAssignableFrom(c2); + c2FromC1 = c2.IsAssignableFrom(c1); + } + + if (c1FromC2 == c2FromC1) + { + return 0; + } + + if (c1FromC2) + { + return 2; + } + else + { + return 1; + } + } + } +} \ No newline at end of file diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs index 897bc99a8..4a29b0a71 100644 --- a/API.Tests/ParserTest.cs +++ b/API.Tests/ParserTest.cs @@ -47,7 +47,8 @@ namespace API.Tests [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "0")] [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")] - + [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "0")] + [InlineData("VanDread-v01-c001[MD].zip", "1")] public void ParseVolumeTest(string filename, string expected) { Assert.Equal(expected, ParseVolume(filename)); @@ -95,6 +96,9 @@ namespace API.Tests [InlineData("Tonikaku Kawaii Vol-1 (Ch 01-08)", "Tonikaku Kawaii")] [InlineData("Tonikaku Kawaii (Ch 59-67) (Ongoing)", "Tonikaku Kawaii")] [InlineData("7thGARDEN v01 (2016) (Digital) (danke).cbz", "7thGARDEN")] + [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "Kedouin Makoto - Corpse Party Musume")] + [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 09", "Kedouin Makoto - Corpse Party Musume")] + [InlineData("Goblin Slayer Side Story - Year One 025.5", "Goblin Slayer Side Story - Year One")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, ParseSeries(filename)); @@ -130,7 +134,10 @@ namespace API.Tests [InlineData("Black Bullet - v4 c20.5 [batoto]", "20.5")] [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1-6")] [InlineData("APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", "40")] + [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "12")] [InlineData("Vol 1", "0")] + [InlineData("VanDread-v01-c001[MD].zip", "1")] + [InlineData("Goblin Slayer Side Story - Year One 025.5", "25.5")] //[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "6")] public void ParseChaptersTest(string filename, string expected) { @@ -278,6 +285,14 @@ namespace API.Tests FullFilePath = filepath }); + filepath = @"E:\Manga\Corpse Party Musume\Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz"; + expected.Add(filepath, new ParserInfo + { + Series = "Corpse Party Musume - Coprse Party", Volumes = "0", Edition = "", + Chapters = "9", Filename = "Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz", Format = MangaFormat.Archive, + FullFilePath = filepath + }); + diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index dea8e47fe..567c7e6a9 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -1,7 +1,32 @@ -namespace API.Tests.Services +using API.Interfaces; +using API.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace API.Tests.Services { + public class DirectoryServiceTests { - + private readonly DirectoryService _directoryService; + private readonly ILogger _logger = Substitute.For>(); + + public DirectoryServiceTests() + { + _directoryService = new DirectoryService(_logger); + } + + [Fact] + public void GetFiles_Test() + { + //_directoryService.GetFiles() + } + + [Fact] + public void ListDirectory_Test() + { + + } } } \ No newline at end of file diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index eeb7ae560..70b4d7e83 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -1,27 +1,37 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using API.Entities; +using API.Entities.Enums; using API.Interfaces; +using API.Interfaces.Services; +using API.Parser; using API.Services; using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using Xunit; +using Xunit.Abstractions; namespace API.Tests.Services { public class ScannerServiceTests { + private readonly ITestOutputHelper _testOutputHelper; private readonly ScannerService _scannerService; private readonly ILogger _logger = Substitute.For>(); private readonly IUnitOfWork _unitOfWork = Substitute.For(); private readonly IArchiveService _archiveService = Substitute.For(); - //private readonly IDirectoryService _directoryService = Substitute.For(); + private readonly IMetadataService _metadataService; + private readonly ILogger _metadataLogger = Substitute.For>(); private Library _libraryMock; - public ScannerServiceTests() + public ScannerServiceTests(ITestOutputHelper testOutputHelper) { - _scannerService = new ScannerService(_unitOfWork, _logger, _archiveService); + _testOutputHelper = testOutputHelper; + _scannerService = new ScannerService(_unitOfWork, _logger, _archiveService, _metadataService); + _metadataService= Substitute.For(_unitOfWork, _metadataLogger, _archiveService); _libraryMock = new Library() { Id = 1, @@ -59,6 +69,7 @@ namespace API.Tests.Services new Series() {Id = 4, Name = "Akame Ga Kill"}, }; Assert.Equal(_libraryMock.Series.ElementAt(0).Id, ScannerService.ExistingOrDefault(_libraryMock, allSeries, "Darker Than Black").Id); + Assert.Equal(_libraryMock.Series.ElementAt(0).Id, ScannerService.ExistingOrDefault(_libraryMock, allSeries, "Darker than Black").Id); } [Fact] @@ -85,31 +96,23 @@ namespace API.Tests.Services Assert.Null(ScannerService.ExistingOrDefault(_libraryMock, allSeries, "Non existing series")); } - // [Fact] - // public void ScanLibrary_Should_Skip() - // { - // - Library lib = new Library() + [Fact] + public void Should_CreateSeries_Test() { - Id = 1, - Name = "Darker Than Black", - Folders = new List() + var allSeries = new List(); + var parsedSeries = new Dictionary>(); + + parsedSeries.Add("Darker Than Black", new List() { - new FolderPath() - { - Id = 1, - LastScanned = DateTime.Now, - LibraryId = 1, - Path = "E:/Manga" - } - }, - LastModified = DateTime.Now - }; - // - // _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1).Returns(lib); - // - // _scannerService.ScanLibrary(1, false); - // } - + new ParserInfo() {Chapters = "0", Filename = "Something.cbz", Format = MangaFormat.Archive, FullFilePath = "E:/Manga/Something.cbz", Series = "Darker Than Black", Volumes = "1"}, + new ParserInfo() {Chapters = "0", Filename = "Something.cbz", Format = MangaFormat.Archive, FullFilePath = "E:/Manga/Something.cbz", Series = "Darker than Black", Volumes = "2"} + }); + + _scannerService.UpsertSeries(_libraryMock, parsedSeries, allSeries); + + Assert.Equal(1, _libraryMock.Series.Count); + Assert.Equal(2, _libraryMock.Series.ElementAt(0).Volumes.Count); + _testOutputHelper.WriteLine(_libraryMock.ToString()); + } } } \ No newline at end of file diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 04c38f75b..ddc9a3b61 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -36,7 +36,7 @@ namespace API.Entities public int Pages { get; set; } // Relationships - public ICollection Volumes { get; set; } + public List Volumes { get; set; } public Library Library { get; set; } public int LibraryId { get; set; } } diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 06ce2df99..31df9ac80 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -91,7 +91,7 @@ namespace API.Services /// Directory to scan /// Action to apply on file path /// - public static int TraverseTreeParallelForEach(string root, Action action) + public static int TraverseTreeParallelForEach(string root, Action action, string searchPattern) { //Count of files traversed and timer for diagnostic output var fileCount = 0; @@ -130,7 +130,7 @@ namespace API.Services // TODO: In future, we need to take LibraryType into consideration for what extensions to allow (RAW should allow images) // or we need to move this filtering to another area (Process) // or we can get all files and put a check in place during Process to abandon files - files = GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions) + files = GetFilesWithCertainExtensions(currentDir, searchPattern) .ToArray(); } catch (UnauthorizedAccessException e) { diff --git a/API/Services/ScannerService.cs b/API/Services/ScannerService.cs index a0a9e7689..5ac321321 100644 --- a/API/Services/ScannerService.cs +++ b/API/Services/ScannerService.cs @@ -67,7 +67,7 @@ namespace API.Services _scannedSeries = null; } - [DisableConcurrentExecution(timeoutInSeconds: 120)] + [DisableConcurrentExecution(timeoutInSeconds: 360)] public void ScanLibrary(int libraryId, bool forceUpdate) { _forceUpdate = forceUpdate; @@ -105,7 +105,7 @@ namespace API.Services { _logger.LogError(exception, $"The file {f} could not be found"); } - }); + }, Parser.Parser.MangaFileExtensions); } catch (ArgumentException ex) { _logger.LogError(ex, $"The directory '{folderPath.Path}' does not exist"); @@ -170,17 +170,18 @@ namespace API.Services { try { + // TODO: I don't need library here. It will always pull from allSeries var mangaSeries = ExistingOrDefault(library, allSeries, seriesKey) ?? new Series { - Name = seriesKey, // NOTE: Should I apply Title casing here + Name = seriesKey, OriginalName = seriesKey, NormalizedName = Parser.Parser.Normalize(seriesKey), SortName = seriesKey, Summary = "" }; mangaSeries.NormalizedName = Parser.Parser.Normalize(mangaSeries.Name); - - + + UpdateSeries(ref mangaSeries, parsedSeries[seriesKey].ToArray()); if (library.Series.Any(s => Parser.Parser.Normalize(s.Name) == mangaSeries.NormalizedName)) continue; _logger.LogInformation($"Added series {mangaSeries.Name}"); @@ -215,6 +216,20 @@ namespace API.Services } _logger.LogInformation($"Removed {count} series that are no longer on disk"); } + + private void RemoveVolumesNotOnDisk(Series series) + { + var volumes = series.Volumes.ToList(); + foreach (var volume in volumes) + { + var chapters = volume.Chapters; + if (!chapters.Any()) + { + series.Volumes.Remove(volume); + //chapters.Select(c => c.Files).Any() + } + } + } /// @@ -260,9 +275,11 @@ namespace API.Services { _logger.LogInformation($"Updating entries for {series.Name}. {infos.Length} related files."); - UpdateVolumes(series, infos); - series.Pages = series.Volumes.Sum(v => v.Pages); + UpdateVolumes(series, infos); + RemoveVolumesNotOnDisk(series); + series.Pages = series.Volumes.Sum(v => v.Pages); + _metadataService.UpdateMetadata(series, _forceUpdate); _logger.LogDebug($"Created {series.Volumes.Count} volumes on {series.Name}"); } @@ -352,10 +369,11 @@ namespace API.Services } - private void UpdateVolumes(Series series, ParserInfo[] infos) + private void UpdateVolumes(Series series, IReadOnlyCollection infos) { + // BUG: If a volume no longer exists, it is not getting deleted. series.Volumes ??= new List(); - _logger.LogDebug($"Updating Volumes for {series.Name}. {infos.Length} related files."); + _logger.LogDebug($"Updating Volumes for {series.Name}. {infos.Count} related files."); var existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList(); foreach (var info in infos)