From c946939c182c92c2a82dac814597c33cdafe2b34 Mon Sep 17 00:00:00 2001 From: Max Obrist Date: Sat, 13 Jun 2026 16:06:16 +0800 Subject: [PATCH 1/3] build: migrate tests to Microsoft.Testing.Platform v2 Swap the VSTest stack for Microsoft.Testing.Platform (MTP) v2: - xunit.v3 -> xunit.v3.mtp-v2; drop Microsoft.NET.Test.Sdk and xunit.runner.visualstudio (the MTP in-proc runner replaces them). - coverlet.collector -> Microsoft.Testing.Extensions.CodeCoverage (native MTP coverage). - Opt into the MTP runner via global.json (test.runner) and set OutputType=Exe for test projects. - Add OutputDirectoryAssemblyResolver: the MTP self-host resolves runtime assemblies strictly from .deps.json, and Scribe (a Scribe.Sdk Library) is referenced as a ProjectReference, so it is not a runtime dep. This xUnit pipeline-startup hook probes the output directory so generators can load Scribe at test time. - Strip the netstandard2.0/framework ambiguous system refs in Scribe.Tests (needed once ValueTask is referenced via xUnit ITestPipelineStartup). - CI: run tests via `dotnet test --solution`, emit native xUnit CTRF and Cobertura coverage, and render both to the job summary via ctrf-io/github-test-reporter and danielpalme/ReportGenerator-GitHub-Action. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-test-core.yml | 48 +++++++++++++++---- Directory.Build.props | 2 + Directory.Packages.props | 9 ++-- .../OutputDirectoryAssemblyResolver.cs | 45 +++++++++++++++++ Scribe.Tests/Scribe.Tests.csproj | 29 +++++++++-- global.json | 5 +- 6 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 Scribe.Tests/OutputDirectoryAssemblyResolver.cs diff --git a/.github/workflows/build-test-core.yml b/.github/workflows/build-test-core.yml index c5a4acf..504c0d6 100644 --- a/.github/workflows/build-test-core.yml +++ b/.github/workflows/build-test-core.yml @@ -76,8 +76,47 @@ jobs: - name: Build run: dotnet build Scribe.slnx --configuration Release --no-restore + # Microsoft.Testing.Platform: tests are self-hosting executables driven by the MTP + # `dotnet test` (opted in via global.json `test.runner`). xUnit's built-in reporter + # emits CTRF test results (no extra package); the CodeCoverage extension emits + # Cobertura. Both are rendered below; the report steps run on always() so a test + # failure still publishes results while the Test step's exit code fails the job. - name: Test - run: dotnet test Scribe.slnx --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" + run: >- + dotnet test --solution Scribe.slnx --configuration Release --no-build --verbosity normal + -- + --report-ctrf --report-ctrf-filename test-results.ctrf.json + --coverage --coverage-output-format cobertura --coverage-output coverage.cobertura.xml + + - name: Publish test report + if: always() + continue-on-error: true + uses: ctrf-io/github-test-reporter@v1 + with: + report-path: '**/test-results.ctrf.json' + + - name: Generate coverage report + if: always() + continue-on-error: true + uses: danielpalme/ReportGenerator-GitHub-Action@5 + with: + reports: '**/coverage.cobertura.xml' + targetdir: coveragereport + reporttypes: MarkdownSummaryGithub;Html + + - name: Publish coverage summary + if: always() + run: | + if [ -f coveragereport/SummaryGithub.md ]; then + cat coveragereport/SummaryGithub.md >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coveragereport - name: Pack core packages if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' @@ -86,13 +125,6 @@ jobs: dotnet pack Scribe.Ink/Scribe.Ink.csproj --configuration Release --no-build --output .artifacts/packages dotnet pack Scribe.Scriptorium/Scribe.Scriptorium.csproj --configuration Release --no-build --output .artifacts/packages - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-core - path: "**/test-results.trx" - - name: Upload packages if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' uses: actions/upload-artifact@v4 diff --git a/Directory.Build.props b/Directory.Build.props index dd819b1..36f6c8c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,6 +12,8 @@ true false + + Exe net10.0 preview enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 37440cd..4f71c0c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,13 +16,12 @@ - + - - + - - + diff --git a/Scribe.Tests/OutputDirectoryAssemblyResolver.cs b/Scribe.Tests/OutputDirectoryAssemblyResolver.cs new file mode 100644 index 0000000..7bb7b6d --- /dev/null +++ b/Scribe.Tests/OutputDirectoryAssemblyResolver.cs @@ -0,0 +1,45 @@ +using System.Runtime.Loader; +using Xunit.v3; + +[assembly: TestPipelineStartup(typeof(Scribe.Tests.OutputDirectoryAssemblyResolver))] + +namespace Scribe.Tests; + +/// +/// Microsoft.Testing.Platform runs this test project as a self-hosted executable that +/// resolves runtime assemblies strictly from its .deps.json. Scribe is consumed +/// here via a ProjectReference to a Scribe.Sdk Library project, whose output +/// is intentionally kept out of consumers' runtime dependency graph — in production it +/// ships embedded as an analyzer, loaded by the Roslyn host, never as an application +/// runtime dependency. VSTest's testhost masked this by probing the output +/// directory; MTP does not, so a test that instantiates a generator touching Scribe throws +/// . +/// +/// Installed via xUnit's pipeline-startup hook rather than a [ModuleInitializer]: +/// the Scribe.Sdk injects polyfilled ModuleInitializerAttribute types into both +/// Scribe and Scribe.Ink, which collide here via InternalsVisibleTo. +/// The assemblies sit in the output directory regardless; this teaches the default load +/// context to probe there for anything missing from the deps.json graph. +/// +public sealed class OutputDirectoryAssemblyResolver : ITestPipelineStartup +{ + public ValueTask StartAsync(Xunit.Sdk.IMessageSink diagnosticMessageSink) + { + AssemblyLoadContext.Default.Resolving += static (context, assemblyName) => + { + if (assemblyName.Name is not { } name) + { + return null; + } + + var candidate = Path.Combine(AppContext.BaseDirectory, name + ".dll"); + return File.Exists(candidate) + ? context.LoadFromAssemblyPath(candidate) + : null; + }; + + return default; + } + + public ValueTask StopAsync() => default; +} diff --git a/Scribe.Tests/Scribe.Tests.csproj b/Scribe.Tests/Scribe.Tests.csproj index b138b25..8a851c8 100644 --- a/Scribe.Tests/Scribe.Tests.csproj +++ b/Scribe.Tests/Scribe.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -7,9 +7,30 @@ - + - - + + + + + <_AmbiguousNetStandardSystemRefs>;System.Buffers.dll;System.Collections.Immutable.dll;System.Memory.dll;System.Numerics.Vectors.dll;System.Reflection.Metadata.dll;System.Runtime.CompilerServices.Unsafe.dll;System.Text.Encoding.CodePages.dll;System.Threading.Tasks.Extensions.dll; + + + + + + diff --git a/global.json b/global.json index 77db41d..9655c62 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,9 @@ "allowPrerelease": false }, "msbuild-sdks": { - "BulletsForHumanity.Scribe.Sdk": "0.6.4" + "BulletsForHumanity.Scribe.Sdk": "0.6.6" + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } From 19fbfce7e6cd0ddff6e1b066dae798ca62bc2e9e Mon Sep 17 00:00:00 2001 From: Max Obrist Date: Sat, 13 Jun 2026 16:19:32 +0800 Subject: [PATCH 2/3] chore: add repo config files to the solution view Surface Directory.Build.props/.targets, Directory.Packages.props, and global.json under Solution Items. Co-Authored-By: Claude Opus 4.8 (1M context) --- Scribe.slnx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Scribe.slnx b/Scribe.slnx index 2f4963b..41ae985 100644 --- a/Scribe.slnx +++ b/Scribe.slnx @@ -1,6 +1,11 @@ + + + + + From 7f913dbff84f414a6d24dc6f975482b8ebb03209 Mon Sep 17 00:00:00 2001 From: Max Obrist Date: Sat, 13 Jun 2026 16:41:48 +0800 Subject: [PATCH 3/3] package upgrades --- .config/dotnet-tools.json | 2 +- Directory.Packages.props | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 49ba0d3..fbfe962 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "csharpier": { - "version": "1.2.6", + "version": "1.3.0", "commands": [ "csharpier" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index 4f71c0c..2e27cfa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + @@ -19,9 +19,9 @@ - + - + \ No newline at end of file