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

Use attribute instead of base class for MSTest #1193

Merged
merged 74 commits into from
May 17, 2024

Conversation

MattKotsenas
Copy link
Contributor

@MattKotsenas MattKotsenas commented Apr 13, 2024

Fixes #1184

Overview

Introduce an attribute [UsesVerify] for MSTest so that tests can set up Verify without needing to use the VerifyBase class. Relaxing the base class requirement makes it easier to use Verify in more cases, where other test scenarios may also require using a base class.

The attribute triggers a source generator that adds the required TestContext property. The setter then stashes the context on an async local for use by the Verifier.

Highlights

  • Created a static Verifier class like the other test frameworks do and added it to the implict usings
  • Removed logic from VerifyBase and forward calls to Verifier
    • This means we can only rely on the information in TestContext to get the Type and MethodInfo used for deriving path info
    • For better performance the Verifier first tries reflection to get the assembly of the test class, and then falls back to assembly scanning
  • Created source generator that adds the TestContext property that sets the AsyncLocal on Verifier
    • Unit tests intentionally do not use the Verify.SourceGenerators to avoid a circular dependency between repos
  • Moved all existing unit tests from VerifyBase to [UsesVerify]
    • Added a simple set of tests to verify the base class doesn't regress
  • Added a dependency on ViHo.PackAsAnalyzer
    • This is the same logic the runtime repo uses to package analyzers / source generators into the main package

Open Issues

  • This PR is big and (in my opinion) difficult to review; do you want me to split it up into preparatory refactoring changes?
    • Calculate name and path info from TestContext and stop relying on VerifyBase inheritance
    • Create the Verifier class and forward calls from VerifyBase
    • Create the actual source generator & update tests
  • Agree on reflection / strategy for finding test Type and MethodInfo
  • Agree on if we want to allow hooking the TestContext setter
  • Agree on using ForAttributeWithMetadataName, which limits users to .NET 7 SDK+ (but can still use any supported TFM)
    • Where should this be documented?
  • Are there other scenarios that should have VerifyBase tests?

src/Verify.MSTest.Tests/Tests.cs Outdated Show resolved Hide resolved
src/Verify.MSTest/Verifier.cs Outdated Show resolved Hide resolved
src/Verify.MSTest.Tests/Tests.cs Outdated Show resolved Hide resolved
@MattKotsenas
Copy link
Contributor Author

MattKotsenas commented May 1, 2024

I haven't forgotten about this. I got pulled into since stuff at work and have to put this on hold for another week or two.

MattKotsenas and others added 28 commits May 14, 2024 16:17
- Code generation attribute missing a quote
- Fix reuse of generic parameter name in test
- Fix incorrect nesting depth braces
/// <param name="testContext">The <see cref="TestContext"/> of the current test.</param>
/// <param name="type">The <see cref="Type"/> of the currently running test.</param>
/// <returns><c>true</c> if the reflection succeeded; <c>false</c> otherwise.</returns>
static bool TryGetTypeFromTestContext(string typeName, TestContext testContext, [NotNullWhen(true)] out Type? type)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why so defensive in this method. IMO it should assume the correct shape and throw if that shape is not correct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hesitant to assert on the specific shape here because I'm reaching into the internals of MSTest and not relying on any public API contracts.

More specifically, I'm concerned that this code would break and become an adoption blocker for early adopters of TestAnywhere (I'm not a user of it myself, but I did verify this code works with the current version).

In general I'm not super thrilled with the amount of reflection going on here, but I think reducing the amount would require digging more into Verify's internals, which feels to me like it should be a separate change.

Thus, I'd prefer leaving it as-is, but I'll do whatever you'd like. I'll volunteer to do follow up work in this space to reduce the amount of reflection overall, but I'd need some direction or a brain dump from you on how to proceed.

Let me know if this makes sense. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An idea that just hit me would be to augment what we stash in the AsyncLocal.

Rather than try to get back to the test class after-the-fact, grab everything we need in the TestContext property setter (which means we're already in the test class). Then Verifier or VerifyBase doesn't need to do any work.

If possible I'd like to keep this PR source generator focused, so if this sounds good to you I can do that as a follow up change, or even as a standalone change and then merge that into this branch.

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An idea that just hit me would be to augment what we stash in the AsyncLocal.

sounds good. i will merge this now. can u have a go at an AsyncLocal PR next?

{
if (classToGenerate.Namespace is not null)
{
builder.Append("namespace ").AppendLine(classToGenerate.Namespace)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a partial revert of the transformation of

builder.Append("namespace ").AppendLine(classToGenerate.Namespace)

to

builder.AppendLine($"namespace {classToGenerate.Namespace}")

(and related changes) for performance.

Because source generators must target netstandard2.0, the underlying StringBuilder doesn't have any interpolated string handling. That means interpolated strings will result in an intermediate allocation.

We could write handling for interpolated strings ourselves, but the code to do so looks nontrivial: https://github.com/dotnet/runtime/pull/51653/files#diff-072e8df6b3d249d58da2b688cbd9369ae5d1e9fb4c1c1da77a19384f3f7711a0.

I also added a comment to the top of IndentedStringBuilder explaining the same decision.

Let me know if you strongly disagree and I can try to find another solution.

@MattKotsenas
Copy link
Contributor Author

OK, all comments addressed. The two items to specifically take a look at are:

  1. Take a look at the note around IndentedStringBuilder allocations
  2. Let me know if you agree / disagree on the TestContext reflection choice

In all other cases I think I took your recommendations / suggestions. If you have any other feedback or thoughts, please let me know. Thanks!

@SimonCropp SimonCropp merged commit 4463576 into VerifyTests:main May 17, 2024
3 checks passed
@SimonCropp SimonCropp added this to the 24.3.0 milestone May 17, 2024
@MattKotsenas MattKotsenas deleted the feature/mstest-no-base branch May 19, 2024 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

[Feature request] Support MSTest without requiring VerifyBase base class
2 participants