Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intermittent System.TypeLoadExceptions and System.Security.VerificationExceptions #1910

Closed
maxcherednik opened this issue Jan 26, 2023 · 13 comments

Comments

@maxcherednik
Copy link

maxcherednik commented Jan 26, 2023

Version: 7.3.1

Castle.Core version: 4.3.1.

After upgrading our solution from .NET 4.8 to .NET 6 we have seen intermittent (about one failed run in every 6-7 build server test runs) System.TypeLoadExceptions and System.Security.VerificationExceptions with the following messages:

System.Security.VerificationException example:

System.ArgumentException  GenericArguments[0], 'CompanyName.OurType[]', on 'T ExecuteLegacyQuery[T](CompanyName.IQuery`1[T])' violates the constraint of type 'T'.
---- System.Security.VerificationException  Method Castle.Proxies.ObjectProxy_35.ExecuteLegacyQuery type argument 'CompanyName.OurType[]' violates the constraint of type parameter 'T'.
   at System.RuntimeType.ValidateGenericArguments(MemberInfo definition, RuntimeType[] genericArguments, Exception e)
   at System.Reflection.RuntimeMethodInfo.MakeGenericMethod(Type[] methodInstantiation)
   at FakeItEasy.Core.MethodInfoManager.MakeGeneric(MethodInfo methodToMakeGeneric, MethodInfo originalMethod) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 112
   at FakeItEasy.Core.MethodInfoManager.c__DisplayClass5_0.GetMethodOnTypeThatImplementsInterfaceMethodb__2(f__AnonymousType4`2 methodTargetPair) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 102
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.TryGetFirst[TSource](IEnumerable`1 source, Boolean& found)
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
   at FakeItEasy.Core.MethodInfoManager.GetMethodOnTypeThatImplementsInterfaceMethod(Type type, MethodInfo method) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 98
   at FakeItEasy.Core.MethodInfoManager.FindMethodOnTypeThatWillBeInvokedByMethodInfo(Type type, MethodInfo method) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 46
   at FakeItEasy.Core.MethodInfoManager.c.GetMethodOnTypeThatWillBeInvokedByMethodInfob__2_0(TypeMethodInfoPair k) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 41
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at FakeItEasy.Core.MethodInfoManager.GetMethodOnTypeThatWillBeInvokedByMethodInfo(Type type, MethodInfo method) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 41
   at FakeItEasy.Creation.CastleDynamicProxy.CastleDynamicProxyInterceptionValidator.GetInvokedMethod(MethodInfo method, Object callTarget) in CprojectsfakeiteasysrcFakeItEasyCreationCastleDynamicProxyCastleDynamicProxyInterceptionValidator.csline 79
   at FakeItEasy.Creation.CastleDynamicProxy.CastleDynamicProxyInterceptionValidator.MethodCanBeInterceptedOnInstance(MethodInfo method, Object callTarget, String& failReason) in CprojectsfakeiteasysrcFakeItEasyCreationCastleDynamicProxyCastleDynamicProxyInterceptionValidator.csline 20
   at FakeItEasy.Creation.FakeObjectCreator.DefaultCreationStrategy.MethodCanBeInterceptedOnInstance(MethodInfo method, Object callTarget, String& failReason) in CprojectsfakeiteasysrcFakeItEasyCreationFakeObjectCreator.csline 184
   at FakeItEasy.Creation.FakeObjectCreator.MethodCanBeInterceptedOnInstance(MethodInfo method, Object callTarget, String& failReason) in CprojectsfakeiteasysrcFakeItEasyCreationFakeObjectCreator.csline 76
   at FakeItEasy.Configuration.DefaultInterceptionAsserter.AssertThatMethodCanBeInterceptedOnInstance(MethodInfo method, Object callTarget) in CprojectsfakeiteasysrcFakeItEasyConfigurationDefaultInterceptionAsserter.csline 19
   at FakeItEasy.Configuration.FakeConfigurationManager.AssertThatMemberCanBeIntercepted(ParsedCallExpression parsed) in CprojectsfakeiteasysrcFakeItEasyConfigurationFakeConfigurationManager.csline 96
   at FakeItEasy.Configuration.FakeConfigurationManager.CallTo[T](Expression`1 callSpecification) in CprojectsfakeiteasysrcFakeItEasyConfigurationFakeConfigurationManager.csline 46
   at FakeItEasy.A.CallTo[T](Expression`1 callSpecification) in CprojectsfakeiteasysrcFakeItEasyA.csline 161

System.TypeLoadException example:

System.TypeLoadException : GenericArguments[0], 'T', on 'Castle.Proxies.Invocations.ISqlDatabase_ExecuteQueryAsync_1[TEvent]' violates the constraint of type parameter 'TEvent'.
   at Castle.Proxies.ObjectProxy_63.ExecuteQueryAsync[T](String sql, Object param, CancellationToken cancellationToken, Nullable`1 commandTimeout)

The common part about all these exceptions is the error about violating the constraint of type parameter and that they are all related to mocks.

I don't believe that this issue directly related to the FakeItEasy, but rather to the underlying library - Castle.Core.

Creating this issue for visibility.

Similar issue with moq4: devlooped/moq#1145

The issue on the Castle.Core side: castleproject/Core#193

@blairconrad
Copy link
Member

Thanks for making us aware, @maxcherednik.

@maxcherednik
Copy link
Author

Wanted to dive into the Castle Core code base, but then realized that FakeItEasy uses slightly older version of it. Any plans to bump the dependency? Maybe the issue does not exist there anymore.

@blairconrad
Copy link
Member

Hi, @maxcherednik. The dependency is the lowest version that FakeItEasy will work with. Leaving it as it is allows users to keep using the older Castle.Core if they have a reason to (perhaps other code relies on this version), while giving them the freedom to attempt to upgrade if they want. All it takes is a PackageReference to the newer Castle.Core. If newer Castle.Core fixes the problem, your pain may be relieved by upgrading. I'd give it a try.

That being said, I don't know that we've tested with the latest Castle.Core, so you may be buying different pain…

@blairconrad
Copy link
Member

blairconrad commented Jan 27, 2023

Okay, I very quickly ran our specs (the main test project) using Castle.Core 5.1.1 on .NET 5.0. 12 tests failed, but it looked like they failed for reasons that would likely not be noticed by a regular user. They were tests that checked our behaviour when certain types couldn't be faked (or couldn't have Dummies made). The creation still fails, but the error message printed is slightly different, due to the Castle.Core changes. I think the gist is that Castle.Core stopped using Castle.DynamicProxy.Generators.GeneratorException in favour of System.ArgumentException. So at this point, I have no reason to believe that a recent FakeItEasy won't work with Castle.Core 5.1.1.

Whether the issue reported here is fixed is another question. (But the Castle.Core issue you pointed to as the likely root cause does remain open…)

@maxcherednik
Copy link
Author

A bit more info which might give us a lead.

I was analysing the test failures and decided to compare the ones which are the same. What I found is that they are not exactly the same.

System.ArgumentException  GenericArguments[0], 'CompanyName.OurType[]', on 'TEvent ExecuteLegacyQuery[TEvent](CompanyName.IQuery`1[TEvent])' violates the constraint of type 'TEvent'.
---- System.Security.VerificationException  Method Castle.Proxies.ObjectProxy_35.ExecuteLegacyQuery type argument 'CompanyName.OurType[]' violates the constraint of type parameter 'TEvent'.
   at System.RuntimeType.ValidateGenericArguments(MemberInfo definition, RuntimeType[] genericArguments, Exception e)
   at System.Reflection.RuntimeMethodInfo.MakeGenericMethod(Type[] methodInstantiation)
   at FakeItEasy.Core.MethodInfoManager.MakeGeneric(MethodInfo methodToMakeGeneric, MethodInfo originalMethod) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 112
   at FakeItEasy.Core.MethodInfoManager.c__DisplayClass5_0.GetMethodOnTypeThatImplementsInterfaceMethodb__2(f__AnonymousType4`2 methodTargetPair) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 102
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.TryGetFirst[TSource](IEnumerable`1 source, Boolean& found)
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
   at FakeItEasy.Core.MethodInfoManager.GetMethodOnTypeThatImplementsInterfaceMethod(Type type, MethodInfo method) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 98
   at FakeItEasy.Core.MethodInfoManager.FindMethodOnTypeThatWillBeInvokedByMethodInfo(Type type, MethodInfo method) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 46
   at FakeItEasy.Core.MethodInfoManager.c.GetMethodOnTypeThatWillBeInvokedByMethodInfob__2_0(TypeMethodInfoPair k) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 41
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at FakeItEasy.Core.MethodInfoManager.GetMethodOnTypeThatWillBeInvokedByMethodInfo(Type type, MethodInfo method) in CprojectsfakeiteasysrcFakeItEasyCoreMethodInfoManager.csline 41
   at FakeItEasy.Creation.CastleDynamicProxy.CastleDynamicProxyInterceptionValidator.GetInvokedMethod(MethodInfo method, Object callTarget) in CprojectsfakeiteasysrcFakeItEasyCreationCastleDynamicProxyCastleDynamicProxyInterceptionValidator.csline 79
   at FakeItEasy.Creation.CastleDynamicProxy.CastleDynamicProxyInterceptionValidator.MethodCanBeInterceptedOnInstance(MethodInfo method, Object callTarget, String& failReason) in CprojectsfakeiteasysrcFakeItEasyCreationCastleDynamicProxyCastleDynamicProxyInterceptionValidator.csline 20
   at FakeItEasy.Creation.FakeObjectCreator.DefaultCreationStrategy.MethodCanBeInterceptedOnInstance(MethodInfo method, Object callTarget, String& failReason) in CprojectsfakeiteasysrcFakeItEasyCreationFakeObjectCreator.csline 184
   at FakeItEasy.Creation.FakeObjectCreator.MethodCanBeInterceptedOnInstance(MethodInfo method, Object callTarget, String& failReason) in CprojectsfakeiteasysrcFakeItEasyCreationFakeObjectCreator.csline 76
   at FakeItEasy.Configuration.DefaultInterceptionAsserter.AssertThatMethodCanBeInterceptedOnInstance(MethodInfo method, Object callTarget) in CprojectsfakeiteasysrcFakeItEasyConfigurationDefaultInterceptionAsserter.csline 19
   at FakeItEasy.Configuration.FakeConfigurationManager.AssertThatMemberCanBeIntercepted(ParsedCallExpression parsed) in CprojectsfakeiteasysrcFakeItEasyConfigurationFakeConfigurationManager.csline 96
   at FakeItEasy.Configuration.FakeConfigurationManager.CallTo[T](Expression`1 callSpecification) in CprojectsfakeiteasysrcFakeItEasyConfigurationFakeConfigurationManager.csline 46
   at FakeItEasy.A.CallTo[T](Expression`1 callSpecification) in CprojectsfakeiteasysrcFakeItEasyA.csline 161

The difference with the first stack trace is:
it is T parameter name vs TEvent parameter name.

Question:
where does this generic parameter name coming from?

The interface looks like this:

internal interface IQueryProcessor
{
        TResult ExecuteLegacyQuery<TResult>(IQuery<TResult> query);
}

internal interface IQuery<TResult>
{
}

@maxcherednik
Copy link
Author

maxcherednik commented Jan 29, 2023

I have another question(lead).
I am thinking now that I might be using dummies incorrectly.

I thought dummies are the cheap fakes. So if I have a dependency, but not planning to do anything with it, then I would use A.Dummy<IQueryProcessor>(); . If I have a logic to verify and mock, then A.Fake<IQueryProcessor>().

In our solution we have both - dummies and fakes of the same interface. In the documentation I can see that dummies are used for simple DTOs.

Questions

  1. Am I using dummies in a wrong way?
  2. Could that wrong way lead to those exceptions?

@blairconrad
Copy link
Member

where does this generic parameter name coming from

No idea. FakeItEasy does not create (or change) generic parameters called "TEvent", as far as I can tell. Nor does a quick search in GitHub reveal anything like that in the Castle.Core source.

@blairconrad
Copy link
Member

I thought dummies are the cheap fakes.

Depends on what you mean by "cheap". If you're looking at performance, it may be more work to create a Dummy than a Fake. As you can see from How are Dummies made?, we go through 7 steps before possibly creating a Fake to act as a Dummy. And if that doesn't work, we'll do even more work.

So if I have a dependency, but not planning to do anything with it, then I would use A.Dummy<IQueryProcessor>()

This is definitely a use. I'd've phrased it more like

an object of a certain type is required, but the actual behavior of the object is not important.

(I'm stealing from the docs). The goal is to signal to the reader of the test "yeah, my API requires me to supply this value, but don't care even a little bit what happens with it in the test. It could be called or not. If called, the return value (if any) or side effect aren't important."

In our solution we have both - dummies and fakes of the same interface. In the documentation I can see that dummies are used for simple DTOs.

I wouldn't characterize them as simple DTOs, since the goal is definitely not to transfer data. The goal is to not care about any data that a Dummy might represent. If you care in any way about the behaviour of your Dummy, I would not use a Dummy. A Fake, configured or not, may be appropriate, depending on the use case. Or maybe not. It really depends on the dependency and production and test code involved.

Am I using dummies in a wrong way?

You may be using them against the intended spirit. Without seeing examples, I can't say for sure.

Could that wrong way lead to those exceptions?

No. Or more accurately, based on my current understanding of how those exceptions are being raised (specifically that I have no idea), I don't think there's any way that you could use a Dummy that would increase the likelihood of the exceptions being raised.

@maxcherednik
Copy link
Author

maxcherednik commented Feb 1, 2023

I have added some logging around the failed tests. We are trying to pin point where there problem is.

I copied some part of the FakeItEasy code to be able to see the MethodInfo:

        var interfaceType = typeof(T);

         var interfaceMap = proxyType.GetTypeInfo().GetRuntimeInterfaceMap(interfaceType);

        interfaceMap.InterfaceMethods
            .Zip(interfaceMap.TargetMethods, (interfaceMethod, targetMethod) => new
            {
                InterfaceMethod = interfaceMethod,
                TargetMethod = targetMethod
            }).ForEach((obj, k) =>
            {
                testOutput.WriteLine($"{k}");
                testOutput.WriteLine($"Target:    {obj.TargetMethod}");
                testOutput.WriteLine($"Interface: {obj.InterfaceMethod}");
                testOutput.WriteLine(string.Empty);
            });

I was trying to find if it is the state of the instance which is broken or if it is the type which is somehow wrong.
For that I use this code snippet and I call it when the failure happens:

 private void ReportBadThings()
        {
            _testOutputHelper.WriteLine("Old dummy-------------------------------------------------");
            ProxyObjectInspector.Inspect<IQueryProcessor>(_dummyQueryProcessor, _testOutputHelper);

            _testOutputHelper.WriteLine("New dummy-------------------------------------------------");
            var newDummyQueryProcessor = A.Dummy<IQueryProcessor>();
            _testOutputHelper.WriteLine($"Same types: {newDummyQueryProcessor.GetType() == _dummyQueryProcessor.GetType()}");
            
            ProxyObjectInspector.Inspect<IQueryProcessor>(newDummyQueryProcessor, _testOutputHelper);

            // if this call succeeds this means that the type is alright and the first instance is somehow broken
            var res1 = newDummyQueryProcessor.ExecuteLegacyQuery(new OrderedAssociationsQuery
            {
                ParentId = Guid.NewGuid()
            });

            _testOutputHelper.WriteLine($"Result from new dummy: {res1}. Count: {res1.Count}");

            var res2 = _dummyQueryProcessor.ExecuteLegacyQuery(new OrderedAssociationsQuery
            {
                ParentId = Guid.NewGuid()
            });

            _testOutputHelper.WriteLine($"Result from the broken dummy: {res2}. Count: {res2.Count}");
        }

What I learned from the output:

  1. if I call another A.Dummy<IQueryProcessor>() It returns the same cached type.
  2. If I call a method on the new instance - it fails with the same exception

Based on this + logs below I can conclude that the issue is the generated type itself.

Good run:

Old dummy-------------------------------------------------
Type: Castle.Proxies.ObjectProxy
Methods:
0: System.String ToString_callback()
1: System.String ToString()
2: Boolean Equals_callback(System.Object)
3: Boolean Equals(System.Object)
4: Int32 GetHashCode_callback()
5: Int32 GetHashCode()
6: TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])
7: System.Object DynProxyGetTarget()
8: Void DynProxySetTarget(System.Object)
9: Castle.DynamicProxy.IInterceptor[] GetInterceptors()
10: System.Type GetType()
11: System.Object MemberwiseClone()
12: Void Finalize()
0
Target:    TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])
Interface: TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])

New dummy-------------------------------------------------
Same types: True
Type: Castle.Proxies.ObjectProxy
Methods:
0: System.String ToString_callback()
1: System.String ToString()
2: Boolean Equals_callback(System.Object)
3: Boolean Equals(System.Object)
4: Int32 GetHashCode_callback()
5: Int32 GetHashCode()
6: TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])
7: System.Object DynProxyGetTarget()
8: Void DynProxySetTarget(System.Object)
9: Castle.DynamicProxy.IInterceptor[] GetInterceptors()
10: System.Type GetType()
11: System.Object MemberwiseClone()
12: Void Finalize()
Target:    TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])
Interface: TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])

Bad run:

Old dummy-------------------------------------------------
Type: Castle.Proxies.ObjectProxy_34
Methods:
0: System.String ToString_callback()
1: System.String ToString()
2: Boolean Equals_callback(System.Object)
3: Boolean Equals(System.Object)
4: Int32 GetHashCode_callback()
5: Int32 GetHashCode()
6: T ExecuteLegacyQuery[T](Core.Foundation.Domain.Querying.IQuery`1[T])
7: System.Object DynProxyGetTarget()
8: Void DynProxySetTarget(System.Object)
9: Castle.DynamicProxy.IInterceptor[] GetInterceptors()
10: System.Type GetType()
11: System.Object MemberwiseClone()
12: Void Finalize()
0
Target:    T ExecuteLegacyQuery[T](Core.Foundation.Domain.Querying.IQuery`1[T])
Interface: TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])

New dummy-------------------------------------------------
Same types: True
Type: Castle.Proxies.ObjectProxy_34
Methods:
0: System.String ToString_callback()
1: System.String ToString()
2: Boolean Equals_callback(System.Object)
3: Boolean Equals(System.Object)
4: Int32 GetHashCode_callback()
5: Int32 GetHashCode()
6: T ExecuteLegacyQuery[T](Core.Foundation.Domain.Querying.IQuery`1[T])
7: System.Object DynProxyGetTarget()
8: Void DynProxySetTarget(System.Object)
9: Castle.DynamicProxy.IInterceptor[] GetInterceptors()
10: System.Type GetType()
11: System.Object MemberwiseClone()
12: Void Finalize()
0
Target:    T ExecuteLegacyQuery[T](Core.Foundation.Domain.Querying.IQuery`1[T])
Interface: TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])

Any idea how this generic argument might be different?

@maxcherednik
Copy link
Author

maxcherednik commented Feb 2, 2023

One more bad run. Now the generic type name is not even T, but TEvent.

Old dummy-------------------------------------------------
Type: Castle.Proxies.ObjectProxy_34
Methods:
0: System.String ToString_callback()
1: System.String ToString()
2: Boolean Equals_callback(System.Object)
3: Boolean Equals(System.Object)
4: Int32 GetHashCode_callback()
5: Int32 GetHashCode()
6: TEvent ExecuteLegacyQuery[TEvent](Core.Foundation.Domain.Querying.IQuery`1[TEvent])
7: System.Object DynProxyGetTarget()
8: Void DynProxySetTarget(System.Object)
9: Castle.DynamicProxy.IInterceptor[] GetInterceptors()
10: System.Type GetType()
11: System.Object MemberwiseClone()
12: Void Finalize()
0
Target:    TEvent ExecuteLegacyQuery[TEvent](Core.Foundation.Domain.Querying.IQuery`1[TEvent])
Interface: TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])

New dummy-------------------------------------------------
Same types: True
Type: Castle.Proxies.ObjectProxy_34
Methods:
0: System.String ToString_callback()
1: System.String ToString()
2: Boolean Equals_callback(System.Object)
3: Boolean Equals(System.Object)
4: Int32 GetHashCode_callback()
5: Int32 GetHashCode()
6: TEvent ExecuteLegacyQuery[TEvent](Core.Foundation.Domain.Querying.IQuery`1[TEvent])
7: System.Object DynProxyGetTarget()
8: Void DynProxySetTarget(System.Object)
9: Castle.DynamicProxy.IInterceptor[] GetInterceptors()
10: System.Type GetType()
11: System.Object MemberwiseClone()
12: Void Finalize()
0
Target:    TEvent ExecuteLegacyQuery[T](Core.Foundation.Domain.Querying.IQuery`1[TEvent])
Interface: TResult ExecuteLegacyQuery[TResult](Core.Foundation.Domain.Querying.IQuery`1[TResult])

@blairconrad
Copy link
Member

blairconrad commented Feb 2, 2023

Thanks for continuing on this, @maxcherednik. I wonder, though. The leading thought is that a problem is in or below the Castle.Core project, it may be a better use of your time to follow up exclusively in castleproject/Core#193. Then you don't have to duplicate your efforts, and people can read all the details in one place…

(The Castle.Core code is largely over my head, and given work and home commitments, I doubt I'm going to get enough time to educate myself and start contributing in the near future.)

@maxcherednik
Copy link
Author

Just leaving a trace for whoever comes after us :)

@blairconrad
Copy link
Member

blairconrad commented Apr 22, 2024

This issue seems to becoming from something in or below Castle.Core, it's unlikely that anything will be done in FakeItEasy to fix it (short of possibly picking up a new Castle.Core if it ends up being fixed there). FakeItEasy maintainers will monitor castleproject/Core#193 and if we need to take action, we will. Until then, closing.

@blairconrad blairconrad closed this as not planned Won't fix, can't repro, duplicate, stale Apr 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants