Date: 2026-08-17
Accepted. Refines 0020 after an independent review of the implemented boundaries. Items 1-6 are the current priority. Polyfill/PolySharp work is gated behind review of items 1-6.
The first framework-neutral extraction introduced DevTools.Testing.Abstractions,
DevTools.Testing.Transport, DevTools.Testing.Host, and
DevTools.Testing.Mtp, but the implementation is not yet neutral end to end:
- generic contracts and host dispatch still contain NUnit identifiers and
nunit/*compatibility routing; - most coherent-generation, runtime-session, assembly-resolution, and unload
mechanics remain in
DevTools.NUnit.Host; DevTools.NUnit.Coremixes generic runner configuration, NUnit discovery and filters, legacy wire DTOs, and the cross-load-context runtime contract;- MTP and VSTest packages link source files from
DevTools.NUnit.Core, creating duplicate compilation and packaging ownership; - the generic result contract loses NUnit hierarchy, skip, attachment, and provider-extension information;
DevTools.TestRunneris a generic executable name over an NUnit-coupled implementation.
The goal is not to genericize NUnit semantics. It is to separate reusable testing control-plane and runtime-isolation mechanisms from provider-owned framework behavior and compatibility workarounds.
-
Make
DevTools.Testing.Abstractionsthe only shared testing identity. It owns framework-neutral run, selection, event, result, attachment, diagnostic, cancellation, and runtime-session contracts. Framework IDs are opaque provider-owned strings; the project contains no NUnit constant. Contracts preserve hierarchy, full name, skip reason, content type, inline attachment content, and a versioned opaque provider payload. It references no MTP, JSON, IPC, framework, reflection, process, or Autodesk assembly. -
Move compatibility out of the generic handler.
DevTools.Testing.Hostregisters and handles onlytesting/*. Legacynunit/*DTOs, protocol negotiation, serialization, and request routing remain temporarily inDevTools.NUnit.TransportandDevTools.NUnit.Host. Host composition may register both handlers, but the generic handler never rewrites a request onto NUnit. -
Extract a policy-driven generation and runtime kernel.
DevTools.Testing.Hostowns immutable generation staging, content hashing, coherent copy/publish retries, manifests, managed/native indexes, current and obsolete generation tracking, active runs, cancellation, session retirement, and unload diagnostics. Providers supply an explicit generation plan and runtime/assembly-resolution policies. The kernel knows no framework DLL name, version, filter, result type, or framework load workaround. -
Keep NUnit policy and semantics in
DevTools.NUnit.*.DevTools.NUnit.Hostowns NUnit 4.6.1 closure validation,nunit.frameworkhost-sharing, NUnit-specific shared/private resolution, selection mapping, provider activation, result mapping, and legacy routing.DevTools.NUnit.Runtimeretains the real NUnit runner, listeners, filters, identities, source locations, result interpretation, output routing, and NUnit lifecycle. It implements the neutral runtime contract without exposing NUnit objects across the load-context boundary. -
Split Runner infrastructure from the NUnit command provider.
DevTools.TestRunnerremains the one executable and composition root. Framework-neutral command parsing, host locate/launch/reuse, debugger policy, and generictesting/*process transport live inDevTools.TestRunner.Core. NUnit CLI (discover/run, filter XML) lives in the executable as anIRunnerCommandModule.DevTools.TestRunner.Coredoes not reference any framework provider. Additional frameworks register another module; they do not require a separate*.Runnerproject. -
Decompose and remove
DevTools.NUnit.Core. Generic runner configuration, paths, timing, and process contracts move toDevTools.Testing.AbstractionsorDevTools.Testing.Transport. NUnit discovery/filter/mapping moves to the appropriate NUnit MTP/Runner provider. Legacy contracts move toDevTools.NUnit.Transport. The cross-load-context runtime contract moves toDevTools.Testing.Abstractions. Source linking from NUnit MTP/VSTest/Runner is removed.DevTools.NUnit.Coreis deleted only after all consumers compile and tests prove equivalent behavior. -
Defer PolySharp migration. The current global
Polyfillpackage creates substantial net48 metadata and duplicate ILRepack inputs. PolySharp may reduce compiler-polyfill noise, but it does not provide the broad downlevel runtime APIs currently supplied byPolyfill, and per-project generation can still create duplicate types. No Polyfill/PolySharp/package/ILRepack policy changes are allowed until items 1-6 pass review. A later isolated spike must prove net48 compilation, metadata validity, ILRepack output, isolated-ALC loading, and host execution before changing [0019].
DevTools.Testing.Abstractions
^
+-- DevTools.Testing.Transport
+-- DevTools.Testing.Host
+-- DevTools.Testing.Mtp
+-- DevTools.NUnit.Runtime
+-- DevTools.NUnit.Host
+-- DevTools.NUnit.MTP
DevTools.TestRunner.Core
^
+-- DevTools.TestRunner (exe; NUnit CLI module + host-family launch)
Forbidden dependency directions:
DevTools.Testing.*to anyDevTools.NUnit.*project;DevTools.TestRunner.Coreto any framework provider;DevTools.NUnit.Runtimeto MTP, Runner, IPC, Autodesk APIs, or host launch;- shared contracts to JSON, reflection, process, or load-context implementation.
The common kernel consumes descriptions and policies rather than framework conditionals:
public sealed record TestingGenerationFile(
string SourcePath,
string RelativePath,
TestingGenerationFileKind Kind);
public sealed record TestingGenerationPlan(
string FrameworkId,
string SourceAssemblyPath,
IReadOnlyList<TestingGenerationFile> Files,
string RuntimeAssemblyRelativePath);
public interface ITestingGenerationPolicy
{
TestingGenerationPlan CreatePlan(string testAssemblyPath);
void ValidatePublished(TestingGenerationManifest manifest);
}
public interface ITestingRuntimeSessionFactory
{
ITestingRuntimeSession Create(TestingGenerationManifest generation);
}Provider policy decides which files enter the plan and how framework closure is validated. The common store decides how that plan is copied, hashed, published, tracked, and retired.
- New execution uses only
testing/*and neutral contracts. - Legacy
nunit/*remains an NUnit-owned adapter until its remaining VSTest callers are removed or deliberately retained by a later decision. - Discovery remains local and cannot launch or contact a host.
- Common result fields must not discard framework information. Data that is not common is preserved in a versioned opaque provider payload.
- A compatibility adapter may map old DTOs to new DTOs exactly once. Generic code must not map a neutral DTO back through an NUnit DTO.
- Rename
DevTools.NUnit.CoretoDevTools.Testing.Core. Rejected because the project contains NUnit discovery, filters, legacy DTOs, and mappings; renaming would hide rather than correct the dependency boundary. - Move all NUnit runtime code into the common kernel. Rejected because NUnit discovery, identity, filters, lifecycle, output, and result semantics must remain framework-owned.
- Keep legacy NUnit routing inside
TestingRequestHandler. Rejected because it makes every future provider inherit an NUnit compatibility path. - Replace Polyfill while moving boundaries. Rejected because assembly movement already changes the ILRepack input closure; combining both changes would make invalid metadata and load failures difficult to attribute.
Positive:
- a future framework provider can reuse host generation/session mechanics without referencing NUnit-named code;
- NUnit DLL-hell workarounds remain intact but isolated behind provider policy;
- the shared loose assembly is a real cross-ALC contract rather than a partial façade over NUnit DTOs;
- source-link duplication and framework leakage become enforceable architecture violations;
- PolySharp can later be evaluated against a stable packaging graph.
Tradeoffs:
- the migration temporarily carries both neutral and legacy NUnit contracts;
- extracting the loader requires parity tests across net48 and modern ALCs;
DevTools.TestRunnerremains a composition root that references the NUnit module even though its reusable core is framework-neutral;- deletion of
DevTools.NUnit.Coreis the final migration step, not the first.
- architecture tests reject NUnit strings/references in
DevTools.Testing.*; - neutral contract round-trip tests preserve all capability fields;
- generation hash, atomic publish, corruption, retry, native asset, session, cancellation, and unload tests pass through the common kernel;
- NUnit host/net48/runtime/MTP/TestAdapter/Runner tests remain green;
- all touched multi-target projects compile for net48, net8, and net10;
- IDE/local discovery proof confirms no host process is opened;
- no Polyfill, PolySharp, or ILRepack policy file changes occur in items 1-6.