// 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.Threading.Tasks;
using AwesomeAssertions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TestFramework.ForTestingMSTest;
namespace MSTestAdapter.PlatformServices.UnitTests.SourceGeneration;
///
/// Verifies that serves the delegate-based
/// invokers published by the AOT source generator (constructor factories, method invokers,
/// property setters) so the adapter runs tests without MethodInfo.Invoke /
/// Activator.CreateInstance / PropertyInfo.SetValue .
///
public sealed class SourceGeneratedReflectionOperationsTests : TestContainer
{
public void GetTestMethodInvoker_ReturnsRegisteredDelegate_AndInvokesWithoutReflection()
{
MethodInfo add = typeof(Sample).GetMethod(nameof(Sample.Add))!;
// Mirrors the source-generated contract: the invoker calls the method directly and returns a
// non-null Task representing completion; the return value is discarded (the method's effect is
// observed via state instead).
Func invoker = static (instance, args) =>
{
((Sample)instance!).Add((int)args![0]!, (int)args![1]!);
return Task.CompletedTask;
};
var provider = new SourceGeneratedReflectionDataProvider
{
TypeMethodInvokers = new Dictionary> { [add] = invoker },
};
var operations = new SourceGeneratedReflectionOperations(provider);
operations.GetTestMethodInvoker(add).Should().BeSameAs(invoker);
var sample = new Sample();
object? result = operations.GetTestMethodInvoker(add)!(sample, [2, 3]);
result.Should().BeAssignableTo();
((Task)result!).IsCompleted.Should().BeTrue();
sample.LastSum.Should().Be(5);
}
public void GetTestMethodInvoker_ReturnsNull_WhenMethodNotRegistered()
{
MethodInfo unregistered = typeof(Sample).GetMethod(nameof(Sample.Add))!;
var operations = new SourceGeneratedReflectionOperations(new SourceGeneratedReflectionDataProvider());
operations.GetTestMethodInvoker(unregistered).Should().BeNull();
}
public void TryGetTestMethodDescriptors_ReturnsRegisteredMethodsAndCompleteness()
{
MethodInfo method = typeof(Sample).GetMethod(nameof(Sample.Add))!;
var provider = new SourceGeneratedReflectionDataProvider
{
DescriptorTestMethods = new Dictionary { [typeof(Sample)] = [method] },
DescriptorCompleteTypes = new Dictionary { [typeof(Sample)] = true },
};
var operations = new SourceGeneratedReflectionOperations(provider);
operations.TryGetTestMethodDescriptors(typeof(Sample), out MethodInfo[]? methods, out bool isComplete)
.Should().BeTrue();
methods.Should().ContainSingle().Which.Should().BeSameAs(method);
isComplete.Should().BeTrue();
}
public void TryGetTestMethodDescriptors_RetainsPerMethodFallbackWhenRegistrationIsIncomplete()
{
MethodInfo add = typeof(Sample).GetMethod(nameof(Sample.Add))!;
MethodInfo subtract = typeof(Sample).GetMethod(nameof(Sample.Subtract))!;
var composite = new CompositeSourceGeneratedReflectionDataProvider();
composite.Add(new SourceGeneratedReflectionDataProvider
{
DescriptorTestMethods = new Dictionary { [typeof(Sample)] = [add] },
DescriptorCompleteTypes = new Dictionary { [typeof(Sample)] = true },
});
composite.Add(new SourceGeneratedReflectionDataProvider
{
DescriptorTestMethods = new Dictionary { [typeof(Sample)] = [subtract] },
DescriptorCompleteTypes = new Dictionary { [typeof(Sample)] = false },
});
var operations = new SourceGeneratedReflectionOperations(composite);
operations.TryGetTestMethodDescriptors(typeof(Sample), out MethodInfo[]? methods, out bool isComplete)
.Should().BeTrue();
methods.Should().HaveCount(2);
methods.Should().Contain(add);
methods.Should().Contain(subtract);
isComplete.Should().BeFalse();
}
public void GetConstructorInvoker_CreatesInstance_WithoutActivator()
{
SourceGeneratedReflectionDataProvider.ConstructorInvoker[] invokers =
[
new SourceGeneratedReflectionDataProvider.ConstructorInvoker
{
Parameters = [typeof(string)],
Invoker = static args => new Sample((string)args![0]!),
},
];
var provider = new SourceGeneratedReflectionDataProvider
{
TypeConstructorsInvoker = new Dictionary { [typeof(Sample)] = invokers },
};
var operations = new SourceGeneratedReflectionOperations(provider);
Func? constructorInvoker = operations.GetConstructorInvoker(typeof(Sample));
constructorInvoker.Should().NotBeNull();
((Sample)constructorInvoker!(["hello"])).Value.Should().Be("hello");
// CreateInstance routes through the same invoker (no Activator.CreateInstance).
((Sample)operations.CreateInstance(typeof(Sample), ["world"])!).Value.Should().Be("world");
}
public void GetConstructorInvoker_ReturnsNull_WhenTypeNotRegistered()
{
var operations = new SourceGeneratedReflectionOperations(new SourceGeneratedReflectionDataProvider());
operations.GetConstructorInvoker(typeof(Sample)).Should().BeNull();
}
public void GetConstructorInvoker_FallsBackToReflection_WhenArgumentsDoNotMatchRegisteredConstructor()
{
// Only the (string) constructor is registered, but Sample also has a parameterless one.
SourceGeneratedReflectionDataProvider.ConstructorInvoker[] invokers =
[
new SourceGeneratedReflectionDataProvider.ConstructorInvoker
{
Parameters = [typeof(string)],
Invoker = static args => new Sample((string)args![0]!),
},
];
var provider = new SourceGeneratedReflectionDataProvider
{
TypeConstructorsInvoker = new Dictionary { [typeof(Sample)] = invokers },
};
var operations = new SourceGeneratedReflectionOperations(provider);
// An empty argument list matches no registered constructor; the invoker must fall back to
// reflection (the parameterless constructor) rather than throwing MissingMethodException.
Func? constructorInvoker = operations.GetConstructorInvoker(typeof(Sample));
constructorInvoker.Should().NotBeNull();
object instance = constructorInvoker!([]);
instance.Should().BeOfType();
((Sample)instance).Value.Should().BeNull();
}
public void GetPropertySetter_AssignsValue_WithoutSetValue()
{
PropertyInfo property = typeof(Sample).GetProperty(nameof(Sample.Value))!;
Action setter = static (instance, value) => ((Sample)instance!).Value = (string?)value;
var provider = new SourceGeneratedReflectionDataProvider
{
TypePropertySetters = new Dictionary> { [property] = setter },
};
var operations = new SourceGeneratedReflectionOperations(provider);
operations.GetPropertySetter(property).Should().BeSameAs(setter);
var sample = new Sample();
operations.GetPropertySetter(property)!(sample, "assigned");
sample.Value.Should().Be("assigned");
}
public void GetPropertySetter_ReturnsNull_WhenPropertyNotRegistered()
{
PropertyInfo property = typeof(Sample).GetProperty(nameof(Sample.Value))!;
var operations = new SourceGeneratedReflectionOperations(new SourceGeneratedReflectionDataProvider());
operations.GetPropertySetter(property).Should().BeNull();
}
public void GetCustomAttributes_ReturnsRegisteredTypeAttributes_IncludingEmptyArray()
{
var generated = new MarkerAttribute("generated");
var provider = new SourceGeneratedReflectionDataProvider
{
TypeAttributes = new Dictionary
{
[typeof(AttributedSample)] = [generated],
[typeof(EmptyAuthoritativeSample)] = [],
},
};
var operations = new SourceGeneratedReflectionOperations(provider);
operations.GetCustomAttributes(typeof(AttributedSample)).Should().ContainSingle().Which.Should().BeSameAs(generated);
operations.GetCustomAttributes(typeof(EmptyAuthoritativeSample)).Should().BeEmpty();
}
public void GetCustomAttributes_FallsBackForMissingTypeEntry()
{
var operations = new SourceGeneratedReflectionOperations(new SourceGeneratedReflectionDataProvider());
operations.GetCustomAttributes(typeof(AttributedSample))
.Should().ContainSingle(attribute => attribute.GetType() == typeof(MarkerAttribute) && ((MarkerAttribute)attribute).Value == "reflection");
}
public void GetCustomAttributes_ReturnsRegisteredMethodAttributes()
{
MethodInfo method = typeof(AttributedSample).GetMethod(nameof(AttributedSample.AttributedMethod))!;
var generated = new MarkerAttribute("generated");
var provider = new SourceGeneratedReflectionDataProvider
{
TypeMethodAttributes = new Dictionary { [method] = [generated] },
};
var operations = new SourceGeneratedReflectionOperations(provider);
operations.GetCustomAttributes(method).Should().ContainSingle().Which.Should().BeSameAs(generated);
}
public void GetCustomAttributes_ReturnsEmptyForAuthoritativeEmptyMethodEntry()
{
MethodInfo method = typeof(AttributedSample).GetMethod(nameof(AttributedSample.AttributedMethod))!;
var provider = new SourceGeneratedReflectionDataProvider
{
TypeMethodAttributes = new Dictionary { [method] = [] },
};
var operations = new SourceGeneratedReflectionOperations(provider);
operations.GetCustomAttributes(method).Should().BeEmpty();
}
public void GetCustomAttributes_FallsBackForMissingMethodEntry()
{
MethodInfo method = typeof(AttributedSample).GetMethod(nameof(AttributedSample.AttributedMethod))!;
var operations = new SourceGeneratedReflectionOperations(new SourceGeneratedReflectionDataProvider());
operations.GetCustomAttributes(method)
.Should().ContainSingle(attribute => attribute.GetType() == typeof(MarkerAttribute) && ((MarkerAttribute)attribute).Value == "reflection");
}
public void MethodAttributeHelpers_UseRegisteredAttributes()
{
MethodInfo method = typeof(AttributedSample).GetMethod(nameof(AttributedSample.AttributedMethod))!;
var generated = new DerivedMarkerAttribute("generated");
var provider = new SourceGeneratedReflectionDataProvider
{
TypeMethodAttributes = new Dictionary { [method] = [generated] },
};
var operations = new SourceGeneratedReflectionOperations(provider);
operations.IsAttributeDefined(method).Should().BeTrue();
// GetFirstAttributeOrDefault is contractually restricted to sealed attribute types (a non-sealed
// type can be matched by several derived attributes), so it is queried with the sealed derived type.
operations.GetFirstAttributeOrDefault(method).Should().BeSameAs(generated);
operations.GetSingleAttributeOrDefault(method).Should().BeSameAs(generated);
operations.GetAttributes(method).Should().ContainSingle().Which.Should().BeSameAs(generated);
}
public void RegisteredAsyncMethodAttributes_PreserveDeclaredAttributesAndRejectAsyncVoid()
{
MethodInfo method = typeof(AsyncAttributedSample).GetMethod(nameof(AsyncAttributedSample.AsyncVoidMethod))!;
var generatedMarker = new MarkerAttribute("generated");
AsyncStateMachineAttribute asyncStateMachineAttribute = method.GetCustomAttribute()!;
var debuggerStepThroughAttribute = new DebuggerStepThroughAttribute();
var provider = new SourceGeneratedReflectionDataProvider
{
TypeMethodAttributes = new Dictionary
{
[method] = [new TestMethodAttribute(), generatedMarker, asyncStateMachineAttribute, debuggerStepThroughAttribute],
},
};
var operations = new SourceGeneratedReflectionOperations(provider);
operations.GetAttributes(method).Should().ContainSingle().Which.Should().BeSameAs(generatedMarker);
operations.GetFirstAttributeOrDefault(method).Should().BeSameAs(asyncStateMachineAttribute);
operations.GetFirstAttributeOrDefault(method).Should().BeSameAs(debuggerStepThroughAttribute);
var validator = new TestMethodValidator(new SourceGeneratedReflectHelper(operations), discoverInternals: false);
var warnings = new List();
validator.IsValidTestMethod(method, typeof(AsyncAttributedSample), warnings).Should().BeFalse();
warnings.Should().ContainSingle();
}
public void MethodAttributeHelpers_GetSingleAttributeOrDefault_ThrowsLocalizedErrorOnDuplicates()
{
MethodInfo method = typeof(AttributedSample).GetMethod(nameof(AttributedSample.AttributedMethod))!;
var provider = new SourceGeneratedReflectionDataProvider
{
TypeMethodAttributes = new Dictionary
{
[method] = [new MarkerAttribute("first"), new DerivedMarkerAttribute("second")],
},
};
var operations = new SourceGeneratedReflectionOperations(provider);
Action action = () => operations.GetSingleAttributeOrDefault(method);
// Regression test for #10302: this path used to throw a hardcoded, non-localizable English message.
action.Should().Throw()
.WithMessage(string.Format(CultureInfo.InvariantCulture, Resource.DuplicateAttributeError, typeof(MarkerAttribute)));
}
public void CompositeProvider_MergesMethodAttributesFromMultipleProviders()
{
MethodInfo firstMethod = typeof(AttributedSample).GetMethod(nameof(AttributedSample.AttributedMethod))!;
MethodInfo secondMethod = typeof(AttributedSample).GetMethod(nameof(AttributedSample.SecondMethod))!;
var firstAttribute = new MarkerAttribute("first");
var secondAttribute = new MarkerAttribute("second");
var composite = new CompositeSourceGeneratedReflectionDataProvider();
composite.Add(new SourceGeneratedReflectionDataProvider
{
TypeMethodAttributes = new Dictionary { [firstMethod] = [firstAttribute] },
});
composite.Add(new SourceGeneratedReflectionDataProvider
{
TypeMethodAttributes = new Dictionary { [secondMethod] = [secondAttribute] },
});
var operations = new SourceGeneratedReflectionOperations(composite);
operations.GetCustomAttributes(firstMethod).Should().ContainSingle().Which.Should().BeSameAs(firstAttribute);
operations.GetCustomAttributes(secondMethod).Should().ContainSingle().Which.Should().BeSameAs(secondAttribute);
}
public void ReflectionMetadataHook_RejectsNullMethodAttributes()
{
Action action = () => ReflectionMetadataHook.Register(
typeof(SourceGeneratedReflectionOperationsTests).Assembly,
[],
new Dictionary(),
new Dictionary(),
[],
null!,
new Dictionary>(),
new Dictionary(),
new Dictionary>());
action.Should().Throw().WithParameterName("methodAttributes");
}
public void ReflectionMetadataHook_RegistersMethodAttributesWithRuntimeProvider()
{
MethodInfo method = typeof(AttributedSample).GetMethod(nameof(AttributedSample.SecondMethod))!;
var generated = new MarkerAttribute("hook");
ReflectionMetadataHook.Register(
typeof(SourceGeneratedReflectionOperationsTests).Assembly,
[typeof(AttributedSample)],
new Dictionary { [typeof(AttributedSample)] = [method] },
new Dictionary { [typeof(AttributedSample)] = [] },
[],
new Dictionary { [method] = [generated] },
new Dictionary>(),
new Dictionary(),
new Dictionary>());
var operations = (SourceGeneratedReflectionOperations)Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.PlatformServiceProvider.Instance.ReflectionOperations;
operations.GetCustomAttributes(method).Should().ContainSingle().Which.Should().BeSameAs(generated);
}
private sealed class Sample
{
public Sample()
{
}
public Sample(string value) => Value = value;
public string? Value { get; set; }
public int LastSum { get; private set; }
public int Add(int first, int second)
{
LastSum = first + second;
return LastSum;
}
public int Subtract(int first, int second)
{
LastSum = first - second;
return LastSum;
}
}
[Marker("reflection")]
private sealed class AttributedSample
{
[Marker("reflection")]
public void AttributedMethod()
{
}
public void SecondMethod()
{
}
}
[Marker("reflection")]
private sealed class EmptyAuthoritativeSample
{
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)]
private class MarkerAttribute(string value) : Attribute
{
public string Value { get; } = value;
}
private sealed class DerivedMarkerAttribute(string value) : MarkerAttribute(value);
private sealed class SourceGeneratedReflectHelper(SourceGeneratedReflectionOperations operations) : ReflectHelper
{
public override bool IsAttributeDefined(ICustomAttributeProvider attributeProvider)
=> operations.IsAttributeDefined(attributeProvider);
public override TAttribute? GetFirstAttributeOrDefault(ICustomAttributeProvider attributeProvider)
where TAttribute : class
=> operations.GetFirstAttributeOrDefault(attributeProvider);
}
private sealed class AsyncAttributedSample
{
[SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Verifies async-void rejection.")]
public async void AsyncVoidMethod() => await Task.Yield();
}
}