Skip to content

fix: ship FSharp.Core with the F# fixture assemblies - #587

Merged
jeremydmiller merged 1 commit into
mainfrom
fix/580-fsharp-core-output
Jul 29, 2026
Merged

fix: ship FSharp.Core with the F# fixture assemblies#587
jeremydmiller merged 1 commit into
mainfrom
fix/580-fsharp-core-output

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #580

Root cause

The issue reported the symptom (no FSharp.Core in FSharpTypes' output, no entry in its deps.json) but not the mechanism. It's in the F# SDK:

<!-- sdk/10.0.101/FSharp/Microsoft.FSharp.NetSdk.props:83-86 -->
<PropertyGroup Condition=" '$(ManagePackageVersionsCentrally)' == 'true' ">
  <DisableImplicitFSharpCoreReference Condition="...">true</DisableImplicitFSharpCoreReference>
</PropertyGroup>

Central package management turns off the F# SDK's implicit FSharp.Core reference. Confirmed directly:

$ dotnet msbuild src/FSharpTypes/FSharpTypes.fsproj -getProperty:DisableImplicitFSharpCoreReference
  "DisableImplicitFSharpCoreReference": "true"

The projects compiled anyway because fsc falls back to the FSharp.Core bundled with whichever SDK is running when nothing else supplies one. That's what made this fail quietly instead of loudly — and it also means the fixtures were being compiled against a different FSharp.Core than the repo pins, varying with the installed SDK.

Reflecting over the output then threw FileNotFoundException for FSharp.Core, Version=10.0.0.0 — the SDK-bundled version it had been compiled against, which was never deployed.

Fix

An explicit <PackageReference Include="FSharp.Core" /> in both F# projects — src/FSharpTypes and src/CodegenTests.FSharpFixture. Under CPM this resolves to the existing central 9.0.303, so it reconciles with CodegenTests' pin rather than adding a second conflicting one, as the issue asked.

src/FSharpCodegenTarget was also flagged as worth checking — it's a C# project, so it never had the gap; it now picks up FSharp.Core transitively.

Verification

Before/after on CoreTests' output directory:

BEFORE:  FSharpTypes.dll
AFTER:   FSharp.Core.dll   FSharpTypes.dll

Answering "what is the fixture actually proving today?"

The issue asked this, and the answer is worth recording: the existing ValueTypeInfoTests F# tests were passing on a broken fixture. typeof() and member lookup never force assembly-level attributes to resolve, so they never touched the missing dependency.

New src/CoreTests/Reflection/FSharpTypesFixtureTests.cs guards the fixture itself. Removing the PackageReference again:

Failed  fsharp_core_is_an_actual_reference_of_the_fixture      System.IO.FileNotFoundException
Failed  the_assembly_level_attributes_can_be_resolved          System.IO.FileNotFoundException
Failed  the_fsharp_types_carry_resolvable_fsharp_core_attributes  System.IO.FileNotFoundException
Passed  every_type_in_the_fixture_can_be_loaded

The exact exception from the issue, and the one that passes either way is GetTypes — which is exactly why this hid for so long. That's noted in the test so nobody mistakes it for a guard.

Test results

Suite Result
CoreTests (net9.0) 479 passed (475 baseline + 4 new)
CoreTests (net10.0) 479 passed
CodegenTests (net9.0) 419 passed
CodegenTests.FSharp compile gate 1 passed

No new compiler warnings — the fixture's four pre-existing FS3261 nullness warnings are unchanged in count.

🤖 Generated with Claude Code

FSharpTypes.dll built to an output directory containing only itself and
its deps.json - no FSharp.Core, and no FSharp.Core entry in deps.json.
The same gap propagated to CoreTests, which project-references it.

Anything that reflected over the assembly threw:

  System.IO.FileNotFoundException: Could not load file or assembly
  'FSharp.Core, Version=10.0.0.0, ...'

because the F# compiler stamps FSharpInterfaceDataVersionAttribute on
every assembly it emits and that attribute type lives in FSharp.Core.

Cause: Microsoft.FSharp.NetSdk.props sets DisableImplicitFSharpCoreReference
to true whenever ManagePackageVersionsCentrally is on, so the F# SDK's
implicit FSharp.Core reference stopped flowing. Compilation still worked -
fsc silently falls back to the copy bundled with whichever SDK is running -
which is why this failed quietly rather than loudly, and why the fixture
was also being compiled against a different FSharp.Core than the repo pins.

Fix is an explicit PackageReference in both F# projects. Under central
package management it resolves to the existing FSharp.Core 9.0.303 rather
than introducing a second, conflicting pin.

Adds CoreTests/Reflection/FSharpTypesFixtureTests to keep the fixture
reflectable. Three of its four tests fail with the exact
FileNotFoundException above when the reference is removed; the fourth
(GetTypes) passes either way, which is precisely why the gap stayed
invisible - type loading never forces the attributes to resolve.

CoreTests 479 (475 + 4 new) on net9.0 and net10.0, CodegenTests 419,
CodegenTests.FSharp compile gate 1. No new compiler warnings: the four
pre-existing FS3261 nullness warnings in the fixture are unchanged.

Closes #580

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit fecc65a into main Jul 29, 2026
1 of 2 checks passed
jeremydmiller added a commit that referenced this pull request Jul 29, 2026
CommandFactory prints "Searching '<assembly>' for commands" until its
static _hasAppliedExtensions latch flips. Being static, it flips once per
process, so exactly one test in the assembly picks up that banner as a
prefix on its captured console output -- whichever test happens to run
first.

execute_single_command_synchronously and its async sibling both asserted
their trimmed output with an exact ShouldBe, so whichever one drew the
short straw failed:

  theOutput.ToString().Trim()
      should be
  "Big is True, Number is 7"
      but was
  "Searching 'JasperFx, Version=2.36.2.0, ...' for commands
  Big is True, Number is 7"

This turned CI red on #587 (which passed on re-run, same commit) and
again on #589. It is not an MTP problem -- #587 was still on the VSTest
bridge -- but MTP orders tests differently, which is why it resurfaced
immediately after the runner change.

The banner is always a prefix, since discovery runs before the command
does, so anchoring to the end of the output keeps the assertion exactly
as strong while making it order-independent.

This is the same class of bug as the AnsiConsole binding already
documented in this constructor, which #577 fixed for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request Jul 29, 2026
…latform (#589)

* build: move test suites from the VSTest bridge to Microsoft Testing Platform

Follow-up to #577, which moved everything to xunit v3 but deliberately
stayed on the VSTest bridge to keep that migration reviewable.

All nine test projects drop Microsoft.NET.Test.Sdk and
xunit.runner.visualstudio and run as their own MTP executables:
no testhost indirection, faster startup, and xunit v3's own reporting.

Four things the issue didn't anticipate:

1. OutputType=Exe now has to be explicit. Microsoft.NET.Test.Sdk was
   setting it; without it, xunit.v3's targets fail the build outright
   ("xUnit.net v3 test projects must be executable").

2. Microsoft.Testing.Extensions.CodeCoverage is pinned to 18.0.6, not
   the latest 18.9.0. xunit.v3 3.2.2 is built against Microsoft.Testing.
   Platform 1.x -- its packages are literally named xunit.v3.core.mtp-v1.
   CodeCoverage 18.1.0+ moved to MTP 2.x, and the mixed graph dies at
   startup on every single test run with

     TypeLoadException: Could not load type
     'Microsoft.Testing.Platform.Extensions.TestHost.IDataConsumer'
     from assembly 'Microsoft.Testing.Platform, Version=2.3.0.0'

   18.0.6 is the last release on MTP 1.x. The constraint is recorded in
   Directory.Packages.props with the symptom, so the next person to bump
   it knows what they're looking at.

3. TestingPlatformShowTestsFailure is required, and its absence is a
   trap. By default MTP prints only

     error run failed: Tests failed: '.../TestResults/EventStoreTests_net9.0_x64.log'

   and sends the test name, assertion and stack trace to that file. CI
   never publishes it, so a red build carries no indication of what
   broke -- strictly worse than the VSTest bridge, which printed
   failures inline. Caught by deliberately failing a test and reading
   the CI log. With the property set, failures print as

     EventExtensionsTests.cs(32): error test failed:
       EventStoreTests.EventExtensionsTests.deliberately_failing_probe:
       Shouldly.ShouldAssertException : "actual" should be "expected"

4. The MTP properties belong in Directory.Build.props rather than per
   project: TestingPlatformDotnetTestSupport changes how `dotnet test`
   invokes a project, so a solution where only some projects answered to
   MTP would leave `dotnet test` on the .slnx in a mixed mode.

build/Build.cs needed no changes at all. All seven DotNetTest calls work
as-is under MTP, including TestCodegenFSharp's .SetFramework("net9.0").

Verified `./build.sh test` end to end (exit 0), every count matching the
current baseline on every target framework:

  CoreTests      480 (479 + 1 skipped)   CodegenTests   419
  CommandLineTests 295                   EventTests     648
  EventStoreTests   72                   Aspire.Tests    51 (net10.0)
  CodegenTests.FSharp 1                  SourceGenerator.Tests 19
  Events.SourceGenerator.Tests 26

Also verified failures still fail the build, which a silent runner swap
could quietly break: a deliberately failing test produced
"Failed: 1, Passed: 72" and Nuke surfaced it as ProcessException,
'dotnet' exited with code 1. CodegenTests and CommandLineTests, the two
suites carrying [assembly: CollectionBehavior(CollectionPerAssembly)],
pass unchanged.

Closes #581

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: stop two CommandExecutor tests depending on running first

CommandFactory prints "Searching '<assembly>' for commands" until its
static _hasAppliedExtensions latch flips. Being static, it flips once per
process, so exactly one test in the assembly picks up that banner as a
prefix on its captured console output -- whichever test happens to run
first.

execute_single_command_synchronously and its async sibling both asserted
their trimmed output with an exact ShouldBe, so whichever one drew the
short straw failed:

  theOutput.ToString().Trim()
      should be
  "Big is True, Number is 7"
      but was
  "Searching 'JasperFx, Version=2.36.2.0, ...' for commands
  Big is True, Number is 7"

This turned CI red on #587 (which passed on re-run, same commit) and
again on #589. It is not an MTP problem -- #587 was still on the VSTest
bridge -- but MTP orders tests differently, which is why it resurfaced
immediately after the runner change.

The banner is always a prefix, since discovery runs before the command
does, so anchoring to the end of the output keeps the assertion exactly
as strong while making it order-independent.

This is the same class of bug as the AnsiConsole binding already
documented in this constructor, which #577 fixed for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

FSharpTypes.dll ships without FSharp.Core, breaking reflection over it

1 participant