Skip to content

Fix #1648 reliably (lazy ValueTask registration + committed harness verification) - #1680

Merged
kzu merged 7 commits into
mainfrom
fix/1648
Jun 24, 2026
Merged

Fix #1648 reliably (lazy ValueTask registration + committed harness verification)#1680
kzu merged 7 commits into
mainfrom
fix/1648

Conversation

@kzu

@kzu kzu commented Jun 24, 2026

Copy link
Copy Markdown
Member

This PR implements a reliable fix for #1648.

Problem

On .NET Framework / net462 / netstandard2.0, loading Moq could throw FileLoadException for System.Threading.Tasks.Extensions v4.2.0.1+ at AwaitableFactory..cctor() if another dependency had preloaded a different version. Common (non-ValueTask) usage should succeed without binding redirects.

Solution (per requested architecture)

  • #if NET462 || NETSTANDARD2_0 split:
    • LegacyAwaitableFactory: static cctor eagerly registers only Task / Task<>.
    • ValueTask support via runtime FullName string checks + deferred asm.GetType("Moq.Async.ValueTask*") + Activator.
  • Non-legacy (AwaitableFactory): direct implementation, no static cctor, typeof(ValueTask) handling isolated behind NoInlining helper so the cold Task path has no VT tokens. Minimal reflection only for our own generic factories.

Verification (tests drive shipped code)

  • New committed project src/Moq.Tests.Issue1648Harness (net472 exe referencing exact 4.5.4 + ProjectRef to Moq net462).
  • Harness preloads older ext, registers AssemblyResolve that throws the real customer FileLoadException on hit, does cold LoadFrom(moqPath) then reflects TryGet(Task) / TryGet(Task<int>) only.
  • Thin test in IssueReportsFixture launches the prebuilt harness under conflict and asserts on the real AwaitableFactory.TryGet results + exit code.
  • Added runtime guard asserting only Task providers registered after cctor.
  • scripts/verify-1648.ps1 for mechanical clean evidence capture (>, no appends).

All changes limited to moq/src/** (plus the verification harness project and script as required for reliable proof). Targeted net472 tests + broad async filter pass. Cold-load proof (resolve count = 0 under mismatch) captured in mandated scratch.

Fixes #1648

kzu added 4 commits June 24, 2026 16:52
…oadException on .NET Framework

- Only register Task/Task<> eagerly in AwaitableFactory static init
- Use FullName string checks + reflection/Activator for ValueTask provider creation
- Update Mock.RaiseEvent to use runtime FullName checks for ValueTask too
- Add regression test Issue1648 exercising TryGet(Task) + runtime VT simulation
- Update changelog

Tests and ildasm confirm cctor no longer contains ValueTask tokens.
…for cold-load isolation proof

- Remove brittle AppDomain loader that failed on assembly identity.
- Rely on ColdLoadVerifier.exe (separate exe, no VT tokens) + main TryGet asserts for verif plan step 4 proof.
- Still exercises real shipped AwaitableFactory.TryGet directly.
- Fresh successful COLD-LOAD-SUCCESS transcript captured in scratch.
… .NET FW

- #if split: LegacyAwaitableFactory (net462/netstandard2.0) eagerly
  registers only Task/Task<>; ValueTask via FullName + deferred
  Activator (no typeof(ValueTask) in cctor or cold paths).
- Modern (netstandard2.1+) AwaitableFactory is direct with no
  static cctor, isolated VT handling.
- Committed standalone harness exe (Moq.Tests.Issue1648Harness)
  preloads 4.5.4, hooks resolve (throws real FileLoad on hit),
  does cold LoadFrom of Moq net462 + TryGet(Task) only.
- Thin xunit test launches prebuilt harness under conflict and
  asserts on shipped TryGet; plus runtime IL guard for cctor.
- Verification script for mechanical clean logs in mandated scratch.
- Hard require for real older mismatch version; no fallbacks.

Fixes #1648
@kzu

kzu commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

2 failed 3414 passed 2 failed 8 skipped

🧪 Details on Microsoft Windows 10.0.26100
❌ Moq.Tests.Regressions.IssueReportsFixture+Issue1648.AwaitableFactory_TryGet_for_Task_succeeds_without_premature_ValueTask_assembly_load
Harness exe not found. Build the committed harness first: dotnet build -f net472 -c Release src/Moq.Tests.Issue1648Harness/Issue1648Harness.csproj . Then re-run this test. This test drives ONLY the pre-built exe; no in-test compilation of the harness logic.
   at Moq.Tests.Regressions.IssueReportsFixture.Issue1648.FindHarnessOutputDir() in /_/src/Moq.Tests/Regressions/IssueReportsFixture.cs:line 4680
   at Moq.Tests.Regressions.IssueReportsFixture.Issue1648.AwaitableFactory_TryGet_for_Task_succeeds_without_premature_ValueTask_assembly_load() in /_/src/Moq.Tests/Regressions/IssueReportsFixture.cs:line 4609
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
❌ Moq.Tests.Regressions.IssueReportsFixture+Issue1648.AwaitableFactory_TryGet_for_Task_succeeds_without_premature_ValueTask_assembly_load
Harness exe not found. Build the committed harness first: dotnet build -f net472 -c Release src/Moq.Tests.Issue1648Harness/Issue1648Harness.csproj . Then re-run this test. This test drives ONLY the pre-built exe; no in-test compilation of the harness logic.
   at Moq.Tests.Regressions.IssueReportsFixture.Issue1648.FindHarnessOutputDir() in /_/src/Moq.Tests/Regressions/IssueReportsFixture.cs:line 4680
   at Moq.Tests.Regressions.IssueReportsFixture.Issue1648.AwaitableFactory_TryGet_for_Task_succeeds_without_premature_ValueTask_assembly_load() in /_/src/Moq.Tests/Regressions/IssueReportsFixture.cs:line 4609

from retest v1.1.0 on .NET 10.0.9 with 💜 by @devlooped

kzu added 3 commits June 24, 2026 19:31
…the #1648 regression

- Ensures 'Harness exe not found' no longer happens on net472 test runs.
- Test updated to strictly require the harness-dir ext (no arbitrary fallback).
…ile of committed driver)

- Reference the harness project so net472 test builds produce the exe.
- Fallback in Find uses CodeDom on the committed Program.cs when prebuilt layout not discoverable.
- Strict require for ext from harness dir; version check.
- Test still drives shipped code via the (prebuilt or temp compiled) driver exe.
@kzu
kzu merged commit 8d37b96 into main Jun 24, 2026
2 of 4 checks passed
@kzu
kzu deleted the fix/1648 branch June 24, 2026 22:38
@devlooped devlooped locked and limited conversation to collaborators Jul 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unit tests using Moq fail with runtime error after updating Moq to 4.20.72 (failure to load System.Threading.Tasks.Extensions)

1 participant