#5733 handling all arguments for Fact and Theory#5734
#5733 handling all arguments for Fact and Theory#5734thomhurst merged 4 commits intothomhurst:mainfrom
Conversation
Up to standards ✅🟢 Issues
|
There was a problem hiding this comment.
Code Review: PR #5734 — "handling all arguments for Fact and Theory"
The PR addresses a real bug (#5733) where the xUnit-to-TUnit migrator was silently dropping named arguments on [Fact] and [Theory] other than Skip (e.g. [Fact(DisplayName = "...")] was migrated to [Test] without a [DisplayName("...")]). The general direction — looping over all named arguments rather than handling only Skip — is correct. However, the implementation has several correctness and architectural concerns.
Critical Bug: Blind Attribute Name Forwarding
The new code treats every named argument as if its name directly maps to a TUnit attribute name:
additionalAttributes.Add(new AdditionalAttribute(
argument.NameEquals!.Name.Identifier.ValueText, ...));This is not safe as a general strategy. xUnit and TUnit do not share the same attribute argument namespace. For example:
[Fact(Timeout = 5000)]→ emits[Timeout(5000)]— but xUnit'sTimeoutis in milliseconds and TUnit's[Timeout]may have a different signature or not exist at all.- Any unrecognized xUnit argument would emit a broken/invalid attribute silently.
Suggested fix: Use an explicit allowlist/mapping table (similar to how NUnitTwoPhaseAnalyzer maps TestName → DisplayName):
private static readonly Dictionary<string, string> FactArgToTUnitAttr = new()
{
{ "Skip", "Skip" },
{ "DisplayName", "DisplayName" },
// Timeout has no direct TUnit equivalent — needs TODO comment
};For unknown arguments, emit a // TODO: TUnit migration — unrecognized argument '{name}' could not be converted comment rather than silently dropping or blindly forwarding.
Silent Data Loss: Positional Arguments Discarded
if (argument.NameEquals is null) { continue; }Positional arguments are silently dropped with no warning. While xUnit's Fact/Theory don't normally take positional args, any edge case (subclass attributes, unusual invocations) will be silently broken. A // TODO diagnostic comment would be safer.
Stale Comments (Misleading for Future Maintainers)
Two comments now contradict the code:
- The final return block still reads
// Remove the Skip argumentbut now applies to all arguments, not justSkip. - The early-return null-ArgumentList path has
// Remove any argumentsbut there are no arguments to remove —// No arguments — plain [Test]would be accurate.
These will confuse future maintainers.
Missing Tests
No test cases were added (confirmed by the PR checklist). The following are needed at minimum:
[Fact(DisplayName = "My Test")]→[Test]+[DisplayName("My Test")][Theory(DisplayName = "My Theory")]→[Test]+[DisplayName("My Theory")]- Regression: existing
Skipbehavior still works
Without tests, the next refactor could silently reintroduce the original bug.
Minor: Eager List Allocation
new List<AdditionalAttribute>(3) is allocated for every [Fact]/[Theory] attribute, even when all arguments are positional (and thus skipped). Lazy allocation (only create the list when a known-good argument is found) would avoid unnecessary allocations in the common case. Low priority, but worth noting given TUnit's performance focus.
Summary
| Bug fix direction | Correct |
| Implementation safety | Needs explicit argument mapping — blind forwarding risks emitting invalid TUnit attributes |
| Silent failures | Positional args and unknown named args are dropped without any diagnostic |
| Test coverage | None added |
| Code clarity | Stale comments mislead future maintainers |
Recommended before merging:
- Add an explicit
string → stringargument-to-attribute mapping with an unknown-arg fallback (TODO comment) - Handle positional args with a TODO comment rather than silently skipping
- Fix the two stale comments
- Add test cases for
DisplayNameon bothFactandTheory
There was a problem hiding this comment.
Code Review: PR #5734 (Updated) — "handling all arguments for Fact and Theory"
This is a follow-up review comparing against my previous comments.
What Was Fixed ✅
All critical concerns from the previous review have been addressed:
-
Blind attribute forwarding eliminated — The new explicit mapping dictionary
_xUnitFactAndTheoryToTUnitArgumentswithTryGetValueis the right approach. Unknown xUnit arguments are now silently dropped rather than blindly emitted as invalid TUnit attributes. -
Tests added —
DisplayNamed_Test_Attributes_Can_Be_FixedandTimeout_Test_Attributes_Can_Be_Fixedcover bothFactandTheoryvariants. The regression test forSkipbehaviour was already present. -
Eager allocation reduced — Using
[]collection expression instead ofnew List<AdditionalAttribute>(3)removes the unnecessary pre-allocation. -
Stale "Remove the Skip argument" comment — Gone now that the block was rewritten.
Timeout Semantics Verified ✅
I checked TimeoutAttribute.cs — TUnit's [Timeout(int timeoutInMilliseconds)] uses milliseconds, matching xUnit's Timeout property. The migration is semantically correct and the test's use of 3600 is valid.
Minor Remaining Items
1. Positional arguments silently dropped (low priority)
xUnit's Fact/Theory don't take positional args in practice, but a TODO comment would protect future maintainers and custom subclasses:
2. Empty list vs null inconsistency (cosmetic)
When ArgumentList is non-null but no arguments match, the returned result has AdditionalAttributes = [] (empty list). The rest of the codebase (NUnit analyzer, the other xUnit path at line 958) normalizes to null when empty:
This is not a bug — MigrationTransformer.cs:607 correctly guards with != null && Count > 0. But for consistency with the established pattern, you could apply the same normalization. Not a blocker.
Summary
The previous critical concerns are resolved. The implementation is now correct and safe. The remaining items are minor — a TODO comment for dropped positional args would be a good defensive addition, but is not required for correctness.
Good work addressing the feedback.
There was a problem hiding this comment.
Code Review: PR #5734 (Updated) — "handling all arguments for Fact and Theory"
This is a follow-up review comparing against my previous comments.
What Was Fixed
All critical concerns from the previous review have been addressed:
-
Blind attribute forwarding eliminated -- The new explicit mapping dictionary
_xUnitFactAndTheoryToTUnitArgumentswithTryGetValueis the right approach. Unknown xUnit arguments are now silently dropped rather than blindly emitted as invalid TUnit attributes. -
Tests added --
DisplayNamed_Test_Attributes_Can_Be_FixedandTimeout_Test_Attributes_Can_Be_Fixedcover bothFactandTheoryvariants. The regression test forSkipbehaviour was already present. -
Eager allocation reduced -- Using
[]collection expression instead ofnew List<AdditionalAttribute>(3)removes the unnecessary pre-allocation. -
Stale "Remove the Skip argument" comment -- Gone now that the block was rewritten.
Timeout Semantics Verified
I checked TimeoutAttribute.cs -- TUnit's [Timeout(int timeoutInMilliseconds)] uses milliseconds, matching xUnit's Timeout property. The migration is semantically correct and the test's use of 3600 is valid.
Minor Remaining Items
1. Positional arguments silently dropped (low priority)
xUnit's Fact/Theory don't take positional args in practice, but a TODO comment would protect future maintainers handling custom subclasses:
if (argument.NameEquals is null)
{
// TODO: positional arguments on Fact/Theory are not supported -- skipping
continue;
}2. Empty list vs null inconsistency (cosmetic)
When ArgumentList is non-null but no arguments match, the returned result has AdditionalAttributes = [] (empty list). The rest of the codebase (NUnit analyzer, the other xUnit path at line 958) normalizes to null when empty:
AdditionalAttributes = additionalAttributes.Count > 0 ? additionalAttributes : nullThis is not a bug -- MigrationTransformer.cs:607 correctly guards with != null && Count > 0. But for consistency with the established pattern, the same normalization could be applied. Not a blocker.
Summary
The previous critical concerns are resolved. The implementation is now correct and safe. The remaining items are minor -- a TODO comment for dropped positional args would be a good defensive addition, but is not required for correctness.
Good work addressing the feedback.
|
Thanks @inyutin-maxim ! |
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.39.0 to 1.40.5. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.40.5 <!-- Release notes generated using configuration in .github/release.yml at v1.40.5 --> ## What's Changed ### Other Changes * Fix reflection property injection reuse by @thomhurst in thomhurst/TUnit#5763 * fix(assertions): gate IsEqualTo<TValue, TOther> overload to net9+ (#5765) by @thomhurst in thomhurst/TUnit#5767 ### Dependencies * chore(deps): update tunit to 1.40.0 by @thomhurst in thomhurst/TUnit#5762 **Full Changelog**: thomhurst/TUnit@v1.40.0...v1.40.5 ## 1.40.0 <!-- Release notes generated using configuration in .github/release.yml at v1.40.0 --> ## What's Changed ### Other Changes * perf(engine): collapse async forwarding wrappers in test execution (#5714) by @thomhurst in thomhurst/TUnit#5725 * perf(engine): skip Console.Out/Err FlushAsync when no output captured (#5712) by @thomhurst in thomhurst/TUnit#5724 * perf(engine): collapse async state machines on hook cache-hit / empty-hook path (#5713) by @thomhurst in thomhurst/TUnit#5726 * perf: eliminate per-test closure + GetOrAdd factory alloc (#5710) by @thomhurst in thomhurst/TUnit#5727 * perf(engine): replace global lock in EventReceiverRegistry with lock-free CAS by @thomhurst in thomhurst/TUnit#5731 * perf(engine): batch per-test overhead cleanups (#5719) by @thomhurst in thomhurst/TUnit#5730 * #5733 handling all arguments for Fact and Theory by @inyutin-maxim in thomhurst/TUnit#5734 * fix(assertions): prefer string overload of Member() over IEnumerable<char> (#5702) by @thomhurst in thomhurst/TUnit#5721 * fix(migration): preserve comments/XML docs when removing sole attributes (#5698) by @thomhurst in thomhurst/TUnit#5739 * perf(build): trim test TFMs and skip viewer dump by default by @thomhurst in thomhurst/TUnit#5741 * fix(pipeline): skip TestBaseModule frameworks with missing binaries by @thomhurst in thomhurst/TUnit#5752 * feat(assertions): focused diff messages for IsEqualTo/IsEquivalentTo (#5732) by @thomhurst in thomhurst/TUnit#5747 * fix(analyzers): remove incorrect AOT rules TUnit0300/0301/0302 (#5722) by @thomhurst in thomhurst/TUnit#5746 * perf(engine): lazy hook metadata registration (#5448) by @thomhurst in thomhurst/TUnit#5750 * chore(templates): unify TUnit version pinning to 1.* (#5709) by @thomhurst in thomhurst/TUnit#5743 * fix(templates): floating TUnit.Aspire version (#5708) by @thomhurst in thomhurst/TUnit#5742 * fix(assertions): preserve specialised source in .Count(itemAssertion) (#5707) by @thomhurst in thomhurst/TUnit#5749 * feat(assertions): IsEqualTo with implicitly-convertible wrappers (#5720) by @thomhurst in thomhurst/TUnit#5751 * feat(aspire): add ability to manually remove resources by @Odonno in thomhurst/TUnit#5586 * fix(fscheck): register default CancellationToken arbitrary that surfaces TestContext token by @JohnVerheij in thomhurst/TUnit#5758 * fix(engine): allow keyed NotInParallel tests to run alongside unconstrained tests (#5700) by @thomhurst in thomhurst/TUnit#5740 * perf: skip TimeoutHelper wrap when no explicit [Timeout] is set (#5711) by @thomhurst in thomhurst/TUnit#5728 ### Dependencies * chore(deps): update tunit to 1.39.0 by @thomhurst in thomhurst/TUnit#5701 * chore(deps): update aspire to 13.2.4 by @thomhurst in thomhurst/TUnit#5735 * chore(deps): bump postcss from 8.5.6 to 8.5.10 in /docs by @dependabot[bot] in thomhurst/TUnit#5736 * chore(deps): update dependency fscheck to 3.3.3 by @thomhurst in thomhurst/TUnit#5760 ## New Contributors * @inyutin-maxim made their first contribution in thomhurst/TUnit#5734 * @Odonno made their first contribution in thomhurst/TUnit#5586 **Full Changelog**: thomhurst/TUnit@v1.39.0...v1.40.0 Commits viewable in [compare view](thomhurst/TUnit@v1.39.0...v1.40.5). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Description
handling all arguments for Fact and Theory
Related Issue
Fixes #5733
Fixes #
Type of Change
Checklist
Required
TUnit-Specific Requirements
TUnit.Core.SourceGenerator)TUnit.Engine)TUnit.Core.SourceGenerator.Testsand/orTUnit.PublicAPItests.received.txtfiles and accepted them as.verified.txt.verified.txtfiles[DynamicallyAccessedMembers]annotationsdotnet publish -p:PublishAot=trueTesting
dotnet test)Additional Notes