Skip to content

Refactor TestMethod type metadata to a single semantic class identity#8213

Merged
Evangelink merged 9 commits into
mainfrom
copilot/refactor-test-method-type-storage
May 14, 2026
Merged

Refactor TestMethod type metadata to a single semantic class identity#8213
Evangelink merged 9 commits into
mainfrom
copilot/refactor-test-method-type-storage

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 14, 2026

✨ Enhancement

TestMethod was carrying overlapping type metadata (FullClassName, ManagedTypeName, and historically syntactic/declaring context), which made inheritance scenarios harder to reason about. This change consolidates semantic type identity to one source and keeps adapter behavior aligned with that model.

What does this improve?

  • Removes duplicate semantic type storage from TestMethod.
  • Makes semantic vs. syntactic class handling clearer in discovery/execution flows.
  • Simplifies API expectations: semantic class identity is FullClassName.

Why is this valuable?

  • Reduces model redundancy and ambiguity in inherited test scenarios.
  • Improves maintainability by eliminating parallel fields representing the same concept.
  • Makes metadata handling more obvious for callers and future refactors.

Implementation approach:

  • TestMethod model cleanup
    • ManagedTypeName is now derived from FullClassName instead of stored separately.
    • Removed managedTypeName from the main TestMethod constructor.
  • Construction path updates
    • Updated discovery and TestCase conversion paths to construct TestMethod without a separate managed type argument.
    • In TestCase rehydration, semantic type is preferred when present.
  • Contract/documentation alignment
    • Updated ITestMethod comments to reflect that managed type maps to semantic class name (FullClassName).
  • Focused unit test updates
    • Adjusted constructor call sites in unit tests.
    • Added coverage for semantic-type preference and ManagedTypeName/FullClassName equivalence.
internal sealed class TestMethod : ITestMethod
{
    public string FullClassName { get; }

    // No separate backing field anymore.
    public string? ManagedTypeName => FullClassName;
}

Copilot AI requested review from Copilot and removed request for Copilot May 14, 2026 09:24
Copilot AI linked an issue May 14, 2026 that may be closed by this pull request
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 14, 2026 09:34
Copilot AI changed the title [WIP] Refactor and cleanup how we store type information for test methods Refactor TestMethod type metadata to a single semantic class identity May 14, 2026
Copilot AI requested a review from Evangelink May 14, 2026 09:36
@Evangelink Evangelink marked this pull request as ready for review May 14, 2026 10:27
Copilot AI review requested due to automatic review settings May 14, 2026 10:27
@Evangelink
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 14, 2026

Expert Code Review (command) failed. Please review the logs for details.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Consolidates TestMethod's type metadata so that ManagedTypeName is computed from FullClassName rather than stored as a separate field. The constructor's managedTypeName parameter is removed, and discovery/rehydration paths and tests are updated accordingly. The interface comments are clarified to reflect the new mapping.

Changes:

  • TestMethod.ManagedTypeName becomes a derived property (=> FullClassName); the main constructor drops the managedTypeName parameter, and TypeEnumerator no longer captures it from ManagedNameHelper.
  • TestCaseExtensions.ToUnitTestElementWithUpdatedSource now prefers the managed-type property (semantic type) over the legacy TestClassNameProperty when constructing the TestMethod.
  • ITestMethod doc comments clarify that FullClassName is the semantic class name and that ManagedTypeName maps to it; tests updated for new constructor and added coverage for managed-type preference and equivalence.
Show a summary per file
File Description
src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs Removes managedTypeName constructor parameter and field; ManagedTypeName now returns FullClassName.
src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ObjectModel/ITestMethod.cs Clarifies semantic meaning of FullClassName and notes that ManagedTypeName maps to it.
src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs Discards managedType from ManagedNameHelper and constructs TestMethod without it.
src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs Uses GetManagedType() (falling back to TestClassNameProperty) as the semantic class name for the new TestMethod.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs Adds test asserting ManagedTypeProperty is set from FullClassName.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs Adds test verifying managed type is preferred over TestClassNameProperty.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs Updates helper to new constructor signature.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs Updates GetTestCase to new constructor signature.

Copilot's findings

  • Files reviewed: 8/8 changed files
  • Comments generated: 0

DebugEx.Assert(_type.AssemblyQualifiedName != null, "AssemblyQualifiedName for method is null.");

ManagedNameHelper.GetManagedNameAndHierarchy(method, out string managedType, out string managedMethod, out string?[] hierarchyValues);
ManagedNameHelper.GetManagedNameAndHierarchy(method, out _, out string managedMethod, out string?[] hierarchyValues);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please keep the out string managedType and instead drop the _type.FullName!.

managedType handles generic cases more properly, I think.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider also finding a unit or E2E test that captures the difference in behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot address this comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Addressed in d56a41d. After extensive discussion (see Evangelink's MAJOR finding), we cannot drop _type.FullName! for FullClassName because TypeCache.LoadType calls assembly.GetType(FullClassName) which requires the closed-generic CLR name to instantiate the type — passing the open-generic managed name (e.g. MyClass1) would return an uninstantiable open-generic Type`.

The current resolution:

  • FullClassName keeps _type.FullName! (closed-generic, required for type loading).
  • ManagedTypeName is derived from FullClassName by stripping the generic argument list ([...]), producing the open-generic managed name expected by VSTest's ManagedType property.
  • The unused managedType out parameter is now discarded with out _ and an explanatory comment is added.

Coverage exists in TypeEnumeratorTests.GetTestFromMethodShouldUseClosedFullClassNameAndOpenManagedTypeNameForGenericTypes (verifies both values for DummyGenericTestClass<int>, then round-trips through ToTestCase and TypeCache.GetTestMethodInfo to prove type loading still works) and TestCaseExtensionsTests.ToUnitTestElementShouldParseLegacyClosedGenericFullyQualifiedNameWhenManagedTypeIsOpenGeneric (verifies legacy closed-generic FQN rehydration).

Comment thread src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs Outdated
@Evangelink
Copy link
Copy Markdown
Member

@copilot address review comments

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 14, 2026

@copilot address review comments

Addressed the review feedback in f1c37dd.

Amaury Levé and others added 2 commits May 14, 2026 15:15
The PR removed the leading `managedTypeName` parameter from the
`TestMethod` constructor (it is now derived from `FullClassName`),
but two test helper methods and one new test still used the old
8-argument signature, breaking the Windows build:

- `UnitTestDiscovererTests.CreateTestMethod` and
  `UnitTestRunnerTests.CreateTestMethod`: passed `typeFullName` twice
  (once as the now-removed `managedTypeName` and once as
  `fullClassName`). Drop the leading argument so they target the new
  7-arg overload.
- `TypeEnumeratorTests.GetTestFromMethodShouldUseManagedTypeNameForGenericTypes`:
  used `nameof(DummyGenericTestClass<int>.GenericTestMethod)` which
  triggers IDE0340 (Use unbound generic type). Replace with
  `nameof(DummyGenericTestClass<>.GenericTestMethod)`.

Also merges the latest `main` into the PR branch to pick up build
infrastructure changes that the previous CI run did not have.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 14, 2026 13:55
Address review comment #3240781680: explain why ToUnitTestElementWithUpdatedSource still falls back to TestClassNameProperty when GetManagedType() returns null. The fallback is required for TestCases produced by older runners that pre-date the managed-type properties (UnitTestElement.ToTestCase does not write the managed-type pair when ManagedMethodName is unknown).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new

@Evangelink
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 14, 2026

Expert Code Review (command) completed successfully!

Expert-reviewer launched in background for PR #8213. It will post the review directly.

Copy link
Copy Markdown
Member

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

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

Review Summary

This PR has a clear goal — eliminate the redundant managedTypeName constructor parameter and derive ManagedTypeName from FullClassName — and the non-generic path is clean. However, the change introduces two behavioural regressions for generic test classes that need to be addressed before merging.


Verdict table

# Dimension Finding Severity
1 Algorithmic Correctness TypeEnumerator now passes managedType (open-generic name) as fullClassName. TypeCache.LoadType uses this to call assembly.GetType(typeName), which returns an uninstantiable open-generic Type for closed-generic test classes. MAJOR
2 Backward Compatibility GetTestName strips the class-name prefix from the FQN using string comparison. Old TestCases produced before this change carry a closed-generic FQN; the new code supplies the open-generic managed type as testClassName. StartsWith fails → method name is set to the entire FQN, breaking re-execution. MAJOR
3 API Contract ManagedTypeName is now always non-null. HasManagedMethodAndTypeProperties / [MemberNotNullWhen] implicitly relied on either half being nullable to gate ManagedTypeProperty writes. The widened write-set is undocumented. MODERATE
4 Test Completeness New generic-type test only validates property values; it does not round-trip through ToTestCase + TypeCache resolution, which is the only path where the bug would surface. MODERATE
5 Assertion Quality GetPropertyValue without type arg returns object?; inconsistent with the rest of the file. NIT
6 Threading/Concurrency N/A — no shared mutable state introduced.
7 Security/IPC N/A — no wire-format changes.
8 Public API All changed members are internal; no PublicAPI.Unshipped.txt impact.
9 Performance N/A — ManagedTypeName => FullClassName is a single property read, no allocation.
10 Cross-TFM N/A — no TFM-specific APIs introduced.
11–21 Remaining dimensions No findings.

Recommended fixes

  1. TypeEnumerator.cs: Keep fullClassName = _type.FullName! (the exact runtime type name for closed generics) and pass managedType only where a managed-type identifier is expected (i.e. ManagedMethodName's counterpart). If the intent is truly to unify them, add a test proving that TypeCache can resolve and instantiate the resulting type.

  2. TestCaseExtensions.cs: After changing testClassName to prefer the managed type, add a test that re-hydrates a TestCase whose FQN was generated under the old convention (closed-generic form) and verifies the method name is parsed correctly.

  3. TestMethod.cs (HasManagedMethodAndTypeProperties): Add a comment or rename the property to reflect that it now only guards the ManagedMethodName being present (since ManagedTypeName can never be null).

Generated by Expert Code Review (command) for issue #8213 · ● 10.7M

Comment thread src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs Outdated
Comment thread src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs Outdated
Copilot AI and others added 2 commits May 14, 2026 14:49
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 14, 2026 14:51
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 14, 2026

Review Summary

This PR has a clear goal — eliminate the redundant managedTypeName constructor parameter and derive ManagedTypeName from FullClassName — and the non-generic path is clean....

Addressed in cb20a82. The fix restores closed-generic FullClassName for type loading, derives open managed type metadata from it, covers legacy generic TestCase rehydration, and clarifies the managed metadata gate.

- TypeEnumerator: discard the unused managedType out variable (use 'out _') and add a comment explaining why FullClassName uses _type.FullName! (closed-generic CLR name needed for TypeCache.LoadType) while ManagedTypeName is derived from it.
- TestMethod: drop StringComparison overload of String.IndexOf for net462/uap10.0.16299 compatibility.
- TestCaseExtensionsTests: use unbound generic type for nameof to satisfy IDE0340.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 14, 2026 17:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new

@Evangelink Evangelink merged commit a466400 into main May 14, 2026
19 of 21 checks passed
@Evangelink Evangelink deleted the copilot/refactor-test-method-type-storage branch May 14, 2026 18:14
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

Successfully merging this pull request may close these issues.

Refactor and cleanup how we store type information for test methods

5 participants