From 41247d924a3a039087e6347b0ffb9cd8447e60ef Mon Sep 17 00:00:00 2001 From: Nice3point Date: Thu, 10 Sep 2026 17:33:01 +0300 Subject: [PATCH 1/5] [Injection] Tie the Revit connection to the test host process --- AGENTS.md | 10 ++-- CHANGELOG.md | 12 ++++ .../Executors/RevitThreadExecutor.cs | 15 +++++ .../Nice3point.TUnit.Revit.csproj | 4 ++ source/Nice3point.TUnit.Revit/RevitApiTest.cs | 22 +++---- .../RevitApplicationTest.cs | 60 ++++++++++++++++++- .../RevitConnectionLifetime.cs | 59 ++++++++++++++++++ .../RevitRegistration.cs | 47 +++++++++++++++ .../build/Nice3point.TUnit.Revit.props | 11 ++++ .../ConnectionLifetimeTests.cs | 17 ++++++ .../Nice3point.TUnit.Revit.Tests.csproj | 3 + 11 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs create mode 100644 source/Nice3point.TUnit.Revit/RevitRegistration.cs create mode 100644 source/Nice3point.TUnit.Revit/build/Nice3point.TUnit.Revit.props create mode 100644 tests/Nice3point.TUnit.Revit.Tests/ConnectionLifetimeTests.cs diff --git a/AGENTS.md b/AGENTS.md index 23cc6d8..d8e95a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,8 +6,9 @@ It adds only the Revit execution model on top of TUnit; assertions, attributes, ## Non-negotiables -* One thread owns the Revit API. Every API call runs on the thread that initialized Revit; the executor marshals test bodies and hooks onto it and caps Revit tests to one at a time. Never touch a Revit type off that thread, and never start a second thread or `Task.Run` for Revit work. -* Inject and eject in matched pairs. The application connects once per test session and releases on the matching session-teardown hook. +* One thread owns the Revit API. Every API call runs on the thread that initialized Revit; the executor marshals test bodies and hooks onto it and declares a cap of one Revit test at a time. Never touch a Revit type off that thread, and never start a second thread or `Task.Run` for Revit work. +* Inject and eject in matched pairs, once per test host process. Revit activates once per process, and a host such as Visual Studio Test Explorer runs a test session per run inside one process. The first session that executes a test connects, and `RevitConnectionLifetime` releases when the test application finishes. A process that leaves Revit connected never terminates. +* Revit starts on the first test a session executes, never on discovery. Nothing that runs for a discovery request opens the connection. An IDE that lists the tests of an assembly leaves Revit unstarted. * The package adds the Revit execution model only. It exposes the base classes, the executor, and the injection lifecycle; assertions, attributes, and discovery come from TUnit. Never reimplement what TUnit provides. * Never break the public surface. Deprecate a renamed member with `[Obsolete]`, name the replacement, and keep the member functional. * Mark a member the test platform invokes but consumers must not call `[EditorBrowsable(EditorBrowsableState.Never)]`. @@ -20,8 +21,9 @@ It adds only the Revit execution model on top of TUnit; assertions, attributes, * A process-wide singleton starts one background STA thread and runs a WPF `Dispatcher` on it. The dispatcher pumps the Win32 messages COM marshaling needs and routes `await` continuations through `DispatcherSynchronizationContext`. * `RevitThreadExecutor` is the public entry point. It queues the action onto the thread host and returns a task that completes once the body and its continuations finish. Unwrap the dispatcher operation (`operation.Task.Unwrap()`) to await the continuations. -* A `IParallelLimit` returning `1` holds the Revit thread exclusive; two tests cannot share it. -* `RevitApplicationTest` holds the static `Application`. `RevitApiTest` opens the connection before the session and closes it after, both on the Revit thread. +* `RevitThreadExecutor` declares an `IParallelLimit` of `1` through `ITestRegisteredEventReceiver`. TUnit applies it from the version that forwards the event to an installed executor. A TUnit without that forwarding schedules every Revit test at once, and the bodies interleave at each `await` on the shared thread. The limit belongs to TUnit; never reintroduce it here as an attribute on the base class. +* `RevitApplicationTest` holds the static `Application`. `RevitApiTest` opens the connection before the session, and `RevitConnectionLifetime` closes it after the test application, both on the Revit thread. +* `RevitConnectionLifetime` is an `ITestHostApplicationLifetime`. The package registers it through the `TestingPlatformBuilderHook` item of `build/Nice3point.TUnit.Revit.props`, packed into `build` and `buildTransitive`; a project that writes its own entry point calls `AddRevit` instead. The test project imports the same props, which a project reference does not deliver. ## Repository map diff --git a/CHANGELOG.md b/CHANGELOG.md index 533c413..2d6f480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# Unreleased + +- One Revit connection now serves the whole test host process. + A host that runs a test session per run inside one process, such as Visual Studio Test Explorer in testing platform server mode, failed every run after the first with `BeforeTestSession hook failed: Attempted to write protected memory.` + The new `RevitConnectionLifetime` releases the connection when the test application finishes. + The package registers it through its build props; a consuming project needs no change, and a project with its own entry point calls `AddRevit`. +- Removed the `RevitApiTest.RevitSessionCleanup` hook, which released the connection after every session. +- Revit tests still run without a parallel limit and interleave at every `await` on the shared thread. + The limit `RevitThreadExecutor` declares does not reach the scheduler: TUnit installs an executor without forwarding `ITestRegisteredEventReceiver` to it. + The fix belongs to TUnit and arrives with the version that carries it. + Until then, run the suite with `--maximum-parallel-tests 1`. + # 2027.0.1 - Updated TUnit to 1.44 diff --git a/source/Nice3point.TUnit.Revit/Executors/RevitThreadExecutor.cs b/source/Nice3point.TUnit.Revit/Executors/RevitThreadExecutor.cs index 2448b58..5ba8091 100644 --- a/source/Nice3point.TUnit.Revit/Executors/RevitThreadExecutor.cs +++ b/source/Nice3point.TUnit.Revit/Executors/RevitThreadExecutor.cs @@ -19,6 +19,8 @@ public sealed class RevitThreadExecutor : GenericAbstractExecutor, ITestRegister /// /// Applies the Revit parallel limiter so registered tests never run concurrently. /// + /// The registration context of the test the executor runs. + /// A completed task. public ValueTask OnTestRegistered(TestRegisteredContext context) { context.SetParallelLimiter(RevitCountParallelLimit.Default); @@ -33,11 +35,24 @@ protected override ValueTask ExecuteAsync(Func action) ArgumentNullException.ThrowIfNull(action); return RevitDispatcherThread.Instance.InvokeAsync(action); } + + /// + /// Runs on the Revit thread outside of a test and a hook. + /// + /// The action to run on the Revit thread. + /// A task that completes once the action and its continuations finish. + internal static ValueTask InvokeAsync(Func action) + { + return RevitDispatcherThread.Instance.InvokeAsync(action); + } } /// /// Restricts Revit API tests to a single concurrent execution. /// +/// +/// TUnit keys a limit by its type. Every Revit test of a run shares this one. +/// file sealed class RevitCountParallelLimit : IParallelLimit { /// diff --git a/source/Nice3point.TUnit.Revit/Nice3point.TUnit.Revit.csproj b/source/Nice3point.TUnit.Revit/Nice3point.TUnit.Revit.csproj index 97d1560..d773230 100644 --- a/source/Nice3point.TUnit.Revit/Nice3point.TUnit.Revit.csproj +++ b/source/Nice3point.TUnit.Revit/Nice3point.TUnit.Revit.csproj @@ -35,6 +35,10 @@ + + + + diff --git a/source/Nice3point.TUnit.Revit/RevitApiTest.cs b/source/Nice3point.TUnit.Revit/RevitApiTest.cs index bbd761a..2daed29 100644 --- a/source/Nice3point.TUnit.Revit/RevitApiTest.cs +++ b/source/Nice3point.TUnit.Revit/RevitApiTest.cs @@ -5,9 +5,11 @@ namespace Nice3point.TUnit.Revit; /// /// Represents a test class for executing tests within the Revit environment. -/// This class provides dependency resolution, setup and cleanup methods for initializing and terminating -/// the connection to the Revit API before and after the test session. +/// This class provides dependency resolution and the setup that establishes the connection to the Revit API before the test session. /// +/// +/// closes the connection when the test application finishes. +/// public abstract class RevitApiTest : RevitApplicationTest { /// @@ -15,22 +17,14 @@ public abstract class RevitApiTest : RevitApplicationTest /// This method is executed before the test session begins, ensuring that the /// necessary prerequisites for the tests interacting with the Revit environment are satisfied. /// + /// + /// The first test a session executes triggers the hook. + /// Listing the tests of an assembly executes none, and leaves Revit unstarted. + /// [Before(TestSession)] [HookExecutor] public static void RevitSessionSetup() { InitializeRevitConnection(); } - - /// - /// Cleans up the Revit session by terminating the connection to the Revit API. - /// This method is executed after the test session concludes, ensuring that - /// resources and connections related to the Revit environment are properly released. - /// - [After(TestSession)] - [HookExecutor] - public static void RevitSessionCleanup() - { - TerminateRevitConnection(); - } } diff --git a/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs b/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs index 71f0407..92ba51d 100644 --- a/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs +++ b/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs @@ -7,8 +7,13 @@ namespace Nice3point.TUnit.Revit; /// Represents an abstract base class for tests that require interaction with the Revit application environment. /// Provides methods to initialize and terminate the connection to the Revit application. /// +/// +/// One connection serves the whole test host process. +/// Revit activates once per process, and a host such as Visual Studio Test Explorer runs a test session per run inside one process. +/// public abstract class RevitApplicationTest { + private static readonly Lock ConnectionLock = new(); private static Injector? _injector; /// @@ -16,21 +21,70 @@ public abstract class RevitApplicationTest /// protected static Application Application { get; private set; } = null!; + /// + /// Gets a value indicating whether the process holds an open connection to the Revit application. + /// + internal static bool IsConnected + { + get + { + lock (ConnectionLock) + { + return _injector is not null; + } + } + } + /// /// Initializes the connection to the Revit application. /// + /// The process terminated its connection earlier. + /// + /// The first call of the process opens the connection, and every later call keeps the open one. + /// protected static void InitializeRevitConnection() { - _injector = new Injector(); - Application = _injector.InjectApplication(); + lock (ConnectionLock) + { + if (_injector is not null) + { + return; + } + + var injector = new Injector(); + Application = injector.InjectApplication(); + _injector = injector; + } } /// /// Terminates the connection to the Revit application. /// Frees associated resources and properly closes the interaction with the Revit environment. /// + /// + /// A call without an open connection has no effect. + /// The connection cannot be reopened in the same process. + /// protected static void TerminateRevitConnection() { - _injector?.EjectApplication(); + lock (ConnectionLock) + { + if (_injector is null) + { + return; + } + + _injector.EjectApplication(); + _injector = null; + Application = null!; + } + } + + /// + /// Terminates the connection from outside the test hierarchy. + /// + internal static void ReleaseConnection() + { + TerminateRevitConnection(); } } diff --git a/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs b/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs new file mode 100644 index 0000000..50ccb55 --- /dev/null +++ b/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs @@ -0,0 +1,59 @@ +using System.ComponentModel; +using Microsoft.Testing.Platform.Extensions.TestHost; +using Nice3point.TUnit.Revit.Executors; + +namespace Nice3point.TUnit.Revit; + +/// +/// Releases the Revit connection when the test application finishes. +/// +/// +/// The test platform runs this after the last test session of the process and before the runtime begins shutting down, in the console host of dotnet run and dotnet test as well as in the server host an IDE keeps alive between runs. +/// It is the last point at which the Revit thread still accepts work. +/// A process that leaves Revit connected never terminates. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class RevitConnectionLifetime : ITestHostApplicationLifetime +{ + /// + public string Uid => nameof(RevitConnectionLifetime); + + /// + public string Version => "1.0.0"; + + /// + public string DisplayName => "Revit connection lifetime"; + + /// + public string Description => "Releases the Revit connection on the Revit thread when the test application finishes."; + + /// + public Task IsEnabledAsync() + { + return Task.FromResult(true); + } + + /// + public Task BeforeRunAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + /// + /// A run that opened no connection, such as a discovery request, leaves the Revit thread untouched. + /// + public async Task AfterRunAsync(int exitCode, CancellationToken cancellationToken) + { + if (!RevitApplicationTest.IsConnected) + { + return; + } + + await RevitThreadExecutor.InvokeAsync(() => + { + RevitApplicationTest.ReleaseConnection(); + return default; + }).ConfigureAwait(false); + } +} diff --git a/source/Nice3point.TUnit.Revit/RevitRegistration.cs b/source/Nice3point.TUnit.Revit/RevitRegistration.cs new file mode 100644 index 0000000..1d91a8d --- /dev/null +++ b/source/Nice3point.TUnit.Revit/RevitRegistration.cs @@ -0,0 +1,47 @@ +using System.ComponentModel; +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Extensions.TestHost; + +namespace Nice3point.TUnit.Revit; + +/// +/// Provides extension methods for to add the Revit execution model. +/// +public static class RevitRegistration +{ + /// The to add the extensions to. + extension(ITestApplicationBuilder builder) + { + /// + /// Adds the Revit connection lifetime to the specified . + /// + /// + /// A test project receives this through the build props of the package. + /// A project that writes its own entry point calls it there. + /// + public void AddRevit() + { + builder.TestHost.AddTestHostApplicationLifetime(_ => new RevitConnectionLifetime()); + } + } +} + +/// +/// Adds the Revit execution model to the test application the test platform generates. +/// +/// +/// The TestingPlatformBuilderHook item of the package's build props names this type, and the build step of the test platform compiles a call to into the generated entry point. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class TestingPlatformBuilderHook +{ + /// + /// Adds the Revit extensions to the specified . + /// + /// The to add the extensions to. + /// The command line arguments the test application was started with. + public static void AddExtensions(ITestApplicationBuilder builder, string[] arguments) + { + builder.AddRevit(); + } +} diff --git a/source/Nice3point.TUnit.Revit/build/Nice3point.TUnit.Revit.props b/source/Nice3point.TUnit.Revit/build/Nice3point.TUnit.Revit.props new file mode 100644 index 0000000..0214a1f --- /dev/null +++ b/source/Nice3point.TUnit.Revit/build/Nice3point.TUnit.Revit.props @@ -0,0 +1,11 @@ + + + + + + Nice3point.TUnit.Revit + Nice3point.TUnit.Revit.TestingPlatformBuilderHook + + + + diff --git a/tests/Nice3point.TUnit.Revit.Tests/ConnectionLifetimeTests.cs b/tests/Nice3point.TUnit.Revit.Tests/ConnectionLifetimeTests.cs new file mode 100644 index 0000000..525df7f --- /dev/null +++ b/tests/Nice3point.TUnit.Revit.Tests/ConnectionLifetimeTests.cs @@ -0,0 +1,17 @@ +namespace Nice3point.TUnit.Revit.Tests; + +public sealed class ConnectionLifetimeTests : RevitApiTest +{ + [Test] + public async Task RevitSessionSetup_SecondCallInProcess_KeepsTheOpenConnection() + { + // Arrange + var application = Application; + + // Act + RevitSessionSetup(); + + // Assert + await Assert.That(Application).IsSameReferenceAs(application); + } +} diff --git a/tests/Nice3point.TUnit.Revit.Tests/Nice3point.TUnit.Revit.Tests.csproj b/tests/Nice3point.TUnit.Revit.Tests/Nice3point.TUnit.Revit.Tests.csproj index 89da84b..d4fa3f1 100644 --- a/tests/Nice3point.TUnit.Revit.Tests/Nice3point.TUnit.Revit.Tests.csproj +++ b/tests/Nice3point.TUnit.Revit.Tests/Nice3point.TUnit.Revit.Tests.csproj @@ -27,4 +27,7 @@ + + + \ No newline at end of file From 7f166ab6de71878050dda9225f51452f42569ff9 Mon Sep 17 00:00:00 2001 From: Nice3point Date: Thu, 10 Sep 2026 18:47:45 +0300 Subject: [PATCH 2/5] [Docs] Describe the connection lifetime in the reference register --- AGENTS.md | 4 ++-- source/Nice3point.TUnit.Revit/RevitApiTest.cs | 2 +- source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs | 4 ++-- source/Nice3point.TUnit.Revit/RevitRegistration.cs | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d8e95a0..0317a26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,8 @@ It adds only the Revit execution model on top of TUnit; assertions, attributes, ## Non-negotiables * One thread owns the Revit API. Every API call runs on the thread that initialized Revit; the executor marshals test bodies and hooks onto it and declares a cap of one Revit test at a time. Never touch a Revit type off that thread, and never start a second thread or `Task.Run` for Revit work. -* Inject and eject in matched pairs, once per test host process. Revit activates once per process, and a host such as Visual Studio Test Explorer runs a test session per run inside one process. The first session that executes a test connects, and `RevitConnectionLifetime` releases when the test application finishes. A process that leaves Revit connected never terminates. -* Revit starts on the first test a session executes, never on discovery. Nothing that runs for a discovery request opens the connection. An IDE that lists the tests of an assembly leaves Revit unstarted. +* Inject and eject in matched pairs, once per test host process. Revit activates once per process, and a host such as Visual Studio Test Explorer runs a test session per run inside one process. The first session that executes a test connects, and `RevitConnectionLifetime` releases when the test application finishes. A process that keeps Revit connected never terminates. +* Revit starts on the first test a session executes, never on discovery. Nothing that runs for a discovery request opens the connection. An IDE that lists the tests of an assembly does not start Revit. * The package adds the Revit execution model only. It exposes the base classes, the executor, and the injection lifecycle; assertions, attributes, and discovery come from TUnit. Never reimplement what TUnit provides. * Never break the public surface. Deprecate a renamed member with `[Obsolete]`, name the replacement, and keep the member functional. * Mark a member the test platform invokes but consumers must not call `[EditorBrowsable(EditorBrowsableState.Never)]`. diff --git a/source/Nice3point.TUnit.Revit/RevitApiTest.cs b/source/Nice3point.TUnit.Revit/RevitApiTest.cs index 2daed29..1f121cb 100644 --- a/source/Nice3point.TUnit.Revit/RevitApiTest.cs +++ b/source/Nice3point.TUnit.Revit/RevitApiTest.cs @@ -19,7 +19,7 @@ public abstract class RevitApiTest : RevitApplicationTest /// /// /// The first test a session executes triggers the hook. - /// Listing the tests of an assembly executes none, and leaves Revit unstarted. + /// Test discovery executes no test and does not start Revit. /// [Before(TestSession)] [HookExecutor] diff --git a/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs b/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs index 50ccb55..f17fd15 100644 --- a/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs +++ b/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs @@ -10,7 +10,7 @@ namespace Nice3point.TUnit.Revit; /// /// The test platform runs this after the last test session of the process and before the runtime begins shutting down, in the console host of dotnet run and dotnet test as well as in the server host an IDE keeps alive between runs. /// It is the last point at which the Revit thread still accepts work. -/// A process that leaves Revit connected never terminates. +/// A process that keeps Revit connected never terminates. /// [EditorBrowsable(EditorBrowsableState.Never)] public sealed class RevitConnectionLifetime : ITestHostApplicationLifetime @@ -41,7 +41,7 @@ public Task BeforeRunAsync(CancellationToken cancellationToken) /// /// - /// A run that opened no connection, such as a discovery request, leaves the Revit thread untouched. + /// When the process holds no connection, as after a discovery request, this method returns without dispatching to the Revit thread. /// public async Task AfterRunAsync(int exitCode, CancellationToken cancellationToken) { diff --git a/source/Nice3point.TUnit.Revit/RevitRegistration.cs b/source/Nice3point.TUnit.Revit/RevitRegistration.cs index 1d91a8d..aad532e 100644 --- a/source/Nice3point.TUnit.Revit/RevitRegistration.cs +++ b/source/Nice3point.TUnit.Revit/RevitRegistration.cs @@ -16,8 +16,8 @@ public static class RevitRegistration /// Adds the Revit connection lifetime to the specified . /// /// - /// A test project receives this through the build props of the package. - /// A project that writes its own entry point calls it there. + /// The build props of the package add this extension to a test project. + /// A project that defines its own entry point calls this method from that entry point. /// public void AddRevit() { @@ -27,7 +27,7 @@ public void AddRevit() } /// -/// Adds the Revit execution model to the test application the test platform generates. +/// Provides the hook Microsoft.Testing.Platform calls while it builds the test application. /// /// /// The TestingPlatformBuilderHook item of the package's build props names this type, and the build step of the test platform compiles a call to into the generated entry point. From 05d174d53ef1ccb6e7347cb720bb4eaf729a3f59 Mon Sep 17 00:00:00 2001 From: Nice3point Date: Thu, 10 Sep 2026 18:51:22 +0300 Subject: [PATCH 3/5] [Injection] Reach the connection teardown from the assembly without a wrapper --- source/Nice3point.TUnit.Revit/RevitApplicationTest.cs | 10 +--------- .../Nice3point.TUnit.Revit/RevitConnectionLifetime.cs | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs b/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs index 92ba51d..f729207 100644 --- a/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs +++ b/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs @@ -65,7 +65,7 @@ protected static void InitializeRevitConnection() /// A call without an open connection has no effect. /// The connection cannot be reopened in the same process. /// - protected static void TerminateRevitConnection() + protected internal static void TerminateRevitConnection() { lock (ConnectionLock) { @@ -79,12 +79,4 @@ protected static void TerminateRevitConnection() Application = null!; } } - - /// - /// Terminates the connection from outside the test hierarchy. - /// - internal static void ReleaseConnection() - { - TerminateRevitConnection(); - } } diff --git a/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs b/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs index f17fd15..d3499bb 100644 --- a/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs +++ b/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs @@ -52,7 +52,7 @@ public async Task AfterRunAsync(int exitCode, CancellationToken cancellationToke await RevitThreadExecutor.InvokeAsync(() => { - RevitApplicationTest.ReleaseConnection(); + RevitApplicationTest.TerminateRevitConnection(); return default; }).ConfigureAwait(false); } From 357d44e32b9941602e243d99d5fd6f019f328c4e Mon Sep 17 00:00:00 2001 From: Nice3point Date: Thu, 10 Sep 2026 18:53:29 +0300 Subject: [PATCH 4/5] [Docs] Name the release and describe the host without a product --- AGENTS.md | 2 +- CHANGELOG.md | 10 +++++----- source/Nice3point.TUnit.Revit/RevitApplicationTest.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0317a26..4b0d412 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ It adds only the Revit execution model on top of TUnit; assertions, attributes, ## Non-negotiables * One thread owns the Revit API. Every API call runs on the thread that initialized Revit; the executor marshals test bodies and hooks onto it and declares a cap of one Revit test at a time. Never touch a Revit type off that thread, and never start a second thread or `Task.Run` for Revit work. -* Inject and eject in matched pairs, once per test host process. Revit activates once per process, and a host such as Visual Studio Test Explorer runs a test session per run inside one process. The first session that executes a test connects, and `RevitConnectionLifetime` releases when the test application finishes. A process that keeps Revit connected never terminates. +* Inject and eject in matched pairs, once per test host process. Revit activates once per process, and an IDE that keeps a test host alive starts a test session per run in that process. The first session that executes a test connects, and `RevitConnectionLifetime` releases when the test application finishes. A process that keeps Revit connected never terminates. * Revit starts on the first test a session executes, never on discovery. Nothing that runs for a discovery request opens the connection. An IDE that lists the tests of an assembly does not start Revit. * The package adds the Revit execution model only. It exposes the base classes, the executor, and the injection lifecycle; assertions, attributes, and discovery come from TUnit. Never reimplement what TUnit provides. * Never break the public surface. Deprecate a renamed member with `[Obsolete]`, name the replacement, and keep the member functional. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6f480..f3918cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ -# Unreleased +# 2027.0.2 - One Revit connection now serves the whole test host process. - A host that runs a test session per run inside one process, such as Visual Studio Test Explorer in testing platform server mode, failed every run after the first with `BeforeTestSession hook failed: Attempted to write protected memory.` + An IDE that keeps a test host alive starts a test session per run in that process, and every run after the first failed with `BeforeTestSession hook failed: Attempted to write protected memory.` The new `RevitConnectionLifetime` releases the connection when the test application finishes. The package registers it through its build props; a consuming project needs no change, and a project with its own entry point calls `AddRevit`. - Removed the `RevitApiTest.RevitSessionCleanup` hook, which released the connection after every session. @@ -42,7 +42,7 @@ TUnit initializes Revit with the `English - United States` language. To override ```csharp using Nice3point.Revit.Injector.Attributes; - + [assembly: RevitLanguage("ENU")] ``` @@ -68,7 +68,7 @@ TUnit initializes Revit from `C:\Program Files\Autodesk\Revit {version}` install ```csharp using Nice3point.Revit.Injector.Attributes; - + [assembly: RevitInstallationPath("D:\Autodesk\Revit Preview")] ``` @@ -108,4 +108,4 @@ Enable private Nuget source for testing # 2026.0.0 -Initial release. Enjoy! \ No newline at end of file +Initial release. Enjoy! diff --git a/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs b/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs index f729207..3390def 100644 --- a/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs +++ b/source/Nice3point.TUnit.Revit/RevitApplicationTest.cs @@ -9,7 +9,7 @@ namespace Nice3point.TUnit.Revit; /// /// /// One connection serves the whole test host process. -/// Revit activates once per process, and a host such as Visual Studio Test Explorer runs a test session per run inside one process. +/// Revit activates once per process, and an IDE that keeps a test host alive starts a test session per run in that process. /// public abstract class RevitApplicationTest { From 6211dca8755fb0721e09e9a1027673142d625ce0 Mon Sep 17 00:00:00 2001 From: Nice3point Date: Thu, 10 Sep 2026 18:56:43 +0300 Subject: [PATCH 5/5] [Injection] Add static anonymous methods --- .../Executors/RevitThreadExecutor.cs | 14 +++++--------- source/Nice3point.TUnit.Revit/RevitApiTest.cs | 3 +-- .../RevitConnectionLifetime.cs | 2 +- source/Nice3point.TUnit.Revit/RevitRegistration.cs | 3 +-- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/source/Nice3point.TUnit.Revit/Executors/RevitThreadExecutor.cs b/source/Nice3point.TUnit.Revit/Executors/RevitThreadExecutor.cs index 5ba8091..10235c5 100644 --- a/source/Nice3point.TUnit.Revit/Executors/RevitThreadExecutor.cs +++ b/source/Nice3point.TUnit.Revit/Executors/RevitThreadExecutor.cs @@ -8,11 +8,9 @@ namespace Nice3point.TUnit.Revit.Executors; /// /// /// Revit requires every API call to occur on the same thread that initialised it. -/// All actions are queued to a process-wide STA thread driven by a WPF -/// : it pumps Win32 messages for COM marshaling and routes -/// await continuations back to the same thread through -/// . Concurrent execution is capped -/// at one test at a time to keep exclusive access to the Revit thread. +/// All actions are queued to a process-wide STA thread driven by a WPF : +/// it pumps Win32 messages for COM marshaling and routes await continuations back to the same thread through . +/// Concurrent execution is capped at one test at a time to keep exclusive access to the Revit thread. /// public sealed class RevitThreadExecutor : GenericAbstractExecutor, ITestRegisteredEventReceiver { @@ -67,8 +65,7 @@ internal static ValueTask InvokeAsync(Func action) } /// -/// Hosts the process-wide STA thread used for every Revit API call and dispatches -/// asynchronous actions onto its WPF . +/// Hosts the process-wide STA thread used for every Revit API call and dispatches asynchronous actions onto its WPF . /// file sealed class RevitDispatcherThread { @@ -105,8 +102,7 @@ private RevitDispatcherThread() public static RevitDispatcherThread Instance { get; } = new(); /// - /// Queues on the Revit thread and returns a task that - /// completes once the action and all of its await continuations finish. + /// Queues on the Revit thread and returns a task that completes once the action and all of its await continuations finish. /// public ValueTask InvokeAsync(Func action) { diff --git a/source/Nice3point.TUnit.Revit/RevitApiTest.cs b/source/Nice3point.TUnit.Revit/RevitApiTest.cs index 1f121cb..6b0ddde 100644 --- a/source/Nice3point.TUnit.Revit/RevitApiTest.cs +++ b/source/Nice3point.TUnit.Revit/RevitApiTest.cs @@ -14,8 +14,7 @@ public abstract class RevitApiTest : RevitApplicationTest { /// /// Sets up the Revit session by initializing the connection to the Revit API. - /// This method is executed before the test session begins, ensuring that the - /// necessary prerequisites for the tests interacting with the Revit environment are satisfied. + /// This method is executed before the test session begins, ensuring that the necessary prerequisites for the tests interacting with the Revit environment are satisfied. /// /// /// The first test a session executes triggers the hook. diff --git a/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs b/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs index d3499bb..72b8d12 100644 --- a/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs +++ b/source/Nice3point.TUnit.Revit/RevitConnectionLifetime.cs @@ -50,7 +50,7 @@ public async Task AfterRunAsync(int exitCode, CancellationToken cancellationToke return; } - await RevitThreadExecutor.InvokeAsync(() => + await RevitThreadExecutor.InvokeAsync(static () => { RevitApplicationTest.TerminateRevitConnection(); return default; diff --git a/source/Nice3point.TUnit.Revit/RevitRegistration.cs b/source/Nice3point.TUnit.Revit/RevitRegistration.cs index aad532e..8dce2a5 100644 --- a/source/Nice3point.TUnit.Revit/RevitRegistration.cs +++ b/source/Nice3point.TUnit.Revit/RevitRegistration.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using Microsoft.Testing.Platform.Builder; -using Microsoft.Testing.Platform.Extensions.TestHost; namespace Nice3point.TUnit.Revit; @@ -21,7 +20,7 @@ public static class RevitRegistration /// public void AddRevit() { - builder.TestHost.AddTestHostApplicationLifetime(_ => new RevitConnectionLifetime()); + builder.TestHost.AddTestHostApplicationLifetime(static _ => new RevitConnectionLifetime()); } } }