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

Upgrade xUnit #75068

Merged
merged 12 commits into from
Sep 26, 2024
Merged

Upgrade xUnit #75068

merged 12 commits into from
Sep 26, 2024

Conversation

jaredpar
Copy link
Member

@jaredpar jaredpar commented Sep 12, 2024

One of our NuGet audit alerts was rooted in our xUnit packages as they only had netstandard1.3 libraries. To resolve this alert I moved us to newer versions of the package which had netstandard2.0 libraries.

Upgrading the package revealed two problems in our code base:

  1. The xUnit Assert APIs took advantage of the unmanaged constraint in a fwe places. That is not supported by Visual Basic and leads to errors on import. To work around this I inserted AssertEx thunks that our Visual Basic layer can call through. Longer term we will need to consider adding support for the constraint (issue)
  2. The xUnit APIs significantly increased their nullable reference type annotations. This caught a number of places where the tests were passing potentially null values to APIs that don't accept null. There were enough hits that I added AssertEx methods that handle the nullable case. In a few places though I just suppressed the warning.
  3. Several xUnit APIs are ambiguous in the face of collection expressions and I needed to add casts to disambiguate.

The vulnerability: GHSA-cmhx-cq75-c4mj

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Sep 12, 2024
@jaredpar jaredpar mentioned this pull request Sep 12, 2024
7 tasks
@jaredpar jaredpar marked this pull request as ready for review September 13, 2024 15:48
@jaredpar jaredpar requested review from a team as code owners September 13, 2024 15:48
@jaredpar
Copy link
Member Author

@dotnet/roslyn-compiler, @dotnet/roslyn-ide PTAL

@@ -160,6 +161,9 @@ public static void AreEqual<T>(T expected, T actual, string message = null, IEqu
}
}

public static void Equal<T>(ImmutableArray<T> expected, IEnumerable<T> actual)
Copy link
Member Author

Choose a reason for hiding this comment

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

This change added Assert.Equals<T>(T expected, T actual). This created an ambiguity with the following pattern in our code base:

AssertEx.Equals([1, 2], someImmutableArray);

That call matches both the new overload and the existing overloads that have ImmutableArray<T> as a parameter. Unfortunately, the number of optional parameters is a tie breaker in overload resolution. That meant that the new overload was winning which is undesirable. Added the overloads without optional parameters to get the desired binding behavior.

Copy link
Member

Choose a reason for hiding this comment

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

If that's the pattern, should we perhaps add AssertEx.Equals<T>(ReadOnlySpan<T>, ImmutableArray<T>) instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you help me understand how that will help?

Copy link
Member

Choose a reason for hiding this comment

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

Just avoid creating the immutable array for the expected value.

Copy link
Member Author

Choose a reason for hiding this comment

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

This feeds into the equality that takes IEnumerable<T> though. I could rewrite that as well. IS that what you're suggesting?

Copy link
Member

Choose a reason for hiding this comment

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

I was specifically going off of the pattern you showed above, which is collectionExpression, ImmutableArray. Is that not the pattern?

.vscode/tasks.json Show resolved Hide resolved
eng/Directory.Packages.props Outdated Show resolved Hide resolved
@@ -160,6 +161,9 @@ public static void AreEqual<T>(T expected, T actual, string message = null, IEqu
}
}

public static void Equal<T>(ImmutableArray<T> expected, IEnumerable<T> actual)
Copy link
Member

Choose a reason for hiding this comment

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

If that's the pattern, should we perhaps add AssertEx.Equals<T>(ReadOnlySpan<T>, ImmutableArray<T>) instead?

@jcouv jcouv self-assigned this Sep 13, 2024
@@ -61,7 +61,7 @@ static void Main(string[] args)
}");
var terms = CSharpProximityExpressionsService.GetProximityExpressions(tree, 245, cancellationToken: default);
Assert.NotNull(terms);
AssertEx.Equal(["yy", "xx"], terms);
AssertEx.Equal((string[])["yy", "xx"], terms);
Copy link
Member

Choose a reason for hiding this comment

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

what's going on here? not loving that these IDE tests just got a lot more verbose (which goes against moving to collection exprs in the first place) :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately xUnit introduced overloads that make these ambiguous now. I'm open to suggestions on how to approach this but it's hard to see how to reconcile the platform issues.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a good example of where Equals(ROSpan<T> span, T[] array) won't fix all our issues. The terms value in this case is IList<T>.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Do you know what it was using before? Was this an IEnumerable<T> vs IEnumerable<T> case?

Copy link
Member

Choose a reason for hiding this comment

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

So, this is an AssertEx helper, not an Assert helper. What methods are actually ambiguous here?

Copy link
Member Author

Choose a reason for hiding this comment

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

This will not be helped by the new overload because the RHS is IReadOnlyList<T>

Copy link
Member

Choose a reason for hiding this comment

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

Sure, but this is not a general xUnit API, this is our own extension. So we can just add whatever helpers we need to make it work.

@@ -63,10 +63,10 @@ public async Task TestWorkspaceDiagnosticsWithRemovedAdditionalFile(bool useVSDi
var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics);
Assert.Equal(3, results.Length);

Assert.Empty(results[0].Diagnostics);
AssertEx.Empty(results[0].Diagnostics);
Copy link
Member

Choose a reason for hiding this comment

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

curious why Assert.Empty doesn't wor khere? (or in all the other places changed).

Copy link
Member Author

Choose a reason for hiding this comment

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

@CyrusNajmabadi tried to summarize a lot of the reasons like this in the PR description + doc comments on the methods. In this case it's because xUnit NRT annotated their APIs and Empty, Single, etc ... do not accept null. Given that diagnostics assigns different meaning to null and Empty I didn't want to make broad changes to the test implementation. Instead I added null annotated versions of the methods to AssertEx and redirected the impacted sites to those.

@@ -2138,7 +2138,7 @@ public void RemoveAnalyzerReference()
solution = solution.WithAnalyzerReferences([analyzerRef1, analyzerRef2]);

var solution2 = solution.RemoveAnalyzerReference(analyzerRef1);
AssertEx.Equal([analyzerRef2], solution2.AnalyzerReferences);
AssertEx.Equal((AnalyzerReference[])[analyzerRef2], solution2.AnalyzerReferences);
Copy link
Member

Choose a reason for hiding this comment

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

:'(

is assertex something owned by roslyn? can we just fixup the signatures here so this works? (or make it do a runtime check to do the right thing)?

Copy link
Member Author

Choose a reason for hiding this comment

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

This will not be fixed by the new overload because the RHS is IReadOnlyList<T>

@CyrusNajmabadi
Copy link
Member

Several xUnit APIs are ambiguous in the face of collection expressions and I needed to add casts to disambiguate.

This seems... bad :)

In particular, i validated much of the collection expr work we did against the xunit api to make sure people could use collection exprs with apis like that without an issue. Could you clarify what is not ambiguous there? I'm def worried as that seems like there's a trap where collection exprs can work well with an api, but then easily change into a form that causes problems, causing source-level breaks.

While the user can work around them, the code is def no longer in the desirable form. Leading to worst of both worlds :(

@333fred
Copy link
Member

333fred commented Sep 13, 2024

Clarified with @CyrusNajmabadi offline. In particular, these ambiguities will be solved in xUnit 2.9.1, when that comes out: xunit/xunit#3021.

@CyrusNajmabadi
Copy link
Member

Ok. Fred walked me through this. Unpleasant. but this seems like an acceptable path forward until we get upgraded to xunit 2.9.1. Would we want to just wait on htat?

@333fred
Copy link
Member

333fred commented Sep 13, 2024

Would we want to just wait on htat?

This is a CG alert lying in wait. We need to solve it now, unfortunately.

@jaredpar
Copy link
Member Author

Yeah unfortunately this is all motivated by getting us CG clean.

@CyrusNajmabadi
Copy link
Member

"CG"?

@CyrusNajmabadi
Copy link
Member

Would we want to just wait on htat?

This is a CG alert lying in wait. We need to solve it now, unfortunately.

Could we add an assertex overload akin to the one brad is adding to 2.9.1? Then we can call that, instead of adding all the cats.

@jaredpar
Copy link
Member Author

"CG"?

Component governance. Basically security issues.

Could we add an assertex overload akin to the one brad is adding to 2.9.1? Then we can call that, instead of adding all the cats.

If there is a pattern that works better here I'm happy to add it.

@333fred
Copy link
Member

333fred commented Sep 14, 2024

Could we add an assertex overload akin to the one brad is adding to 2.9.1? Then we can call that, instead of adding all the cats.

If there is a pattern that works better here I'm happy to add it.

That's what I suggested here: #75068 (comment)

@jjonescz
Copy link
Member

jjonescz commented Sep 16, 2024

Clarified with @CyrusNajmabadi offline. In particular, these ambiguities will be solved in xUnit 2.9.1, when that comes out: xunit/xunit#3021.

I didn't follow how the linked issue is related. The first-class span feature isn't merged into main, so it cannot cause ambiguities here.

@CyrusNajmabadi
Copy link
Member

Isn't this just an ambiguity involving collection expressions? The overload we want to add (which will also come in 2.9.1) will be seen as better than the current ambiguous overloads.

@jjonescz
Copy link
Member

Oh yeah, I see, the xunit fix will help with some cases that became ambiguous even without the "first-class span" feature.

@CyrusNajmabadi
Copy link
Member

Yup yup. So my preference is that we add the overload, update our code to call that, then upgrade xunit again, then remove the overload and update again.

@333fred
Copy link
Member

333fred commented Sep 16, 2024

Oh yeah, I see, the xunit fix will help with some cases that became ambiguous even without the "first-class span" feature.

Yup, first-class spans made this slightly more breaking, but it was already broken.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

Done with review pass (iteration 5)

.editorconfig Outdated
@@ -151,6 +151,16 @@ dotnet_public_api_analyzer.require_api_files = true
# Workaround for https://github.com/dotnet/roslyn/issues/70570
dotnet_diagnostic.IDE0055.severity = warning

# Enable the xUnit analyzer rules
Copy link
Member

Choose a reason for hiding this comment

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

Comment says enable, but this is only disabling things.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes but it's referencing an issue that says enable the rules.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks (iteration 5)

@nohwnd
Copy link
Member

nohwnd commented Sep 17, 2024

@jaredpar revert 97e3b and define

<MicrosoftTestPlatformVersion>17.11.1</MicrosoftTestPlatformVersion>
<MicrosoftNETTestSdkVersion>$(MicrosoftTestPlatformVersion)</MicrosoftNETTestSdkVersion>

To update the version of test.sdk from the outdated 17.5 that arcade ships. That fixes the language server tests (details here microsoft/vstest#10355)

And hopefully does not break any other tests.

@jaredpar
Copy link
Member Author

@333fred feedback addressed and tests are passing. Can you give it another look?

@@ -160,6 +161,12 @@ public static void AreEqual<T>(T expected, T actual, string message = null, IEqu
}
}

public static void Equal<T>(ReadOnlySpan<T> expected, T[] actual) =>
Equal<T>(expected.ToArray(), actual);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: since we know allocations are a problem in tests, perhaps instead call Equal<T>(expected, actual.AsSpan())?

One of our NuGet audit alerts was rooted in our xUnit packages as they
only had `netstandard1.3` libraries. To resolve this alert I moved us to
newer versions of the package which had `netstandard2.0` libraries.

Upgrading the package revealed two problems in our code base:

1. The xUnit `Assert` APIs took advantage of the `unmanaged` constraint
   in a fwe places. That is not supported by Visual Basic and leads to
   errors on import. To work around this I inserted `AssertEx` thunks
   that our Visual Basic layer can call through. Longer term we will
   need to consider adding support for the constraint
   ([issue](dotnet#75063))
2. The xUnit APIs significantly increased their nullable reference type
   annotations. This caught a number of places where the tests were
   passing potentially null values to APIs that don't accept null. There
   were enough hits that I added `AssertEx` methods that handle the
   nullable case. In a few places though I just suppressed the warning.

The vulnerability: GHSA-cmhx-cq75-c4mj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead VSCode
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants