Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
05d2d91
Make object containers IAsyncDisposable
Code-Grump Sep 1, 2025
f0e0b96
Fix MSBuild Task package references
Code-Grump Sep 1, 2025
26e7dcb
Add missing task awaiting
Code-Grump Sep 1, 2025
7488f83
Update docs regarding IDisposable and IAsyncDisposable support.
Code-Grump Sep 3, 2025
9f73a4d
Add IAsyncDisposable support to change log
Code-Grump Sep 3, 2025
3394d42
Convert OnScenarioInitialize path to asynchronous
Code-Grump Sep 3, 2025
89de9d2
Switch licence file out for SPDX identifier in NuGet packages
Code-Grump Sep 6, 2025
5881874
Switch MSBuild.Generation project to using csproj for package composi…
Code-Grump Sep 6, 2025
a481888
Clean up self-test capability
Code-Grump Sep 6, 2025
f815d7a
Simplification of direct MSBuild generation integration in tests
Code-Grump Sep 6, 2025
780e0d2
Remove diagnostic messages
Code-Grump Sep 6, 2025
9a731c4
Clean up direct build imports
Code-Grump Sep 6, 2025
2bd9e46
Set up deffered task loading for tests
Code-Grump Sep 6, 2025
9f69115
Simplified code-generation tasks to run late as possible: just before…
Code-Grump Sep 6, 2025
6edd718
Add line breaks for clarity
Code-Grump Sep 6, 2025
898d8fd
Correct issue with analyzer being transiently added to projects
Code-Grump Sep 6, 2025
de8095d
Clean up DLL selection for packing
Code-Grump Sep 8, 2025
0fa2208
Fix resource embedding happening too late in the build process
Code-Grump Sep 10, 2025
a1084d5
Merge branch 'feature/simplified-msbuild-package' into feature/iasync…
Code-Grump Sep 13, 2025
4f6e002
Add awaiting of scenario initialize to core generator
Code-Grump Sep 13, 2025
cc578ed
Remove VisualStudio package references
Code-Grump Sep 13, 2025
4512731
Fix expectation in XUnit generator tests to expect async method name
Code-Grump Sep 13, 2025
17c3595
Merge branch 'main' into feature/iasyncdisposable-support
Code-Grump Sep 24, 2025
5a559fb
Suppress VisualStudio threading analyzer warning
Code-Grump Sep 26, 2025
ce4c2a5
Merge branch 'main' into feature/iasyncdisposable-support
Code-Grump Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# [vNext]

## Improvements:
* Added support for asynchronously disposing objects implementing IAsyncDisposable when the Reqnroll object container is disposed.

## Bug fixes:

*Contributors of this release (in alphabetical order):*
*Contributors of this release (in alphabetical order):* @Code-Grump

# v3.1.0 - 2025-09-26

Expand All @@ -14,7 +15,7 @@
* Disabling parallel execution with the `addNonParallelizableMarkerForTags` efature now also applies to scenario-level tags for frameworks supporting method-level isolation (NUnit, MsTest V2, TUnit). (#826)
* Generating "friendly names" for generated test methods by default can be disabled by the `generator/disableFriendlyTestNames` setting in `reqnroll.json`. This can help to avoid compatiblity issues with tools like VsTest retry. For MsTest this setting restores the behavior of Reqnroll v2. (#854)

## Improvements:
* Dependencies: Updated to Cucumber Gherkin v35.0.0, Cucumber Messages v29.0.0 and Cucumber CompatibilityKit v23.0.0

* Reqnroll.Verify: Support for Verify v29+ (Verify.Xunit v29.0.0 or later). For earlier versions use 3.0.3 version of the plugin that is compatible with Reqnroll v3.*. The support for custom snapshot files with global VerifySettings has been removed, see [plugin documentation](https://docs.reqnroll.net/latest/integrations/verify.html) for details and workarounds. (#572)
* Dependencies: Updated to Cucumber Gherkin v35, Cucumber Messages v29 and Cucumber CompatibilityKit v23 (#841)
Expand Down
2 changes: 1 addition & 1 deletion Reqnroll.Generator/Generation/GeneratorConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public class GeneratorConstants
{
public const string DEFAULT_NAMESPACE = "ReqnrollTests";
public const string TEST_NAME_FORMAT = "{0}";
public const string SCENARIO_INITIALIZE_NAME = "ScenarioInitialize";
public const string SCENARIO_INITIALIZE_NAME = "ScenarioInitializeAsync";
public const string SCENARIO_START_NAME = "ScenarioStartAsync";
public const string SCENARIO_CLEANUP_NAME = "ScenarioCleanupAsync";
public const string TEST_INITIALIZE_NAME = "TestInitializeAsync";
Expand Down
17 changes: 11 additions & 6 deletions Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,14 +521,19 @@ private void SetupScenarioInitializeMethod(TestClassGenerationContext generation
scenarioInitializeMethod.Parameters.Add(
new CodeParameterDeclarationExpression(new CodeTypeReference(typeof(RuleInfo), CodeTypeReferenceOptions.GlobalReference), "ruleInfo"));

_codeDomHelper.MarkCodeMemberMethodAsAsync(scenarioInitializeMethod);

//testRunner.OnScenarioInitialize(scenarioInfo, ruleInfo);
var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression();
scenarioInitializeMethod.Statements.Add(
new CodeMethodInvokeExpression(
testRunnerField,
nameof(ITestRunner.OnScenarioInitialize),
new CodeVariableReferenceExpression("scenarioInfo"),
new CodeVariableReferenceExpression("ruleInfo")));
var expression = new CodeMethodInvokeExpression(
testRunnerField,
nameof(ITestRunner.OnScenarioInitializeAsync),
new CodeVariableReferenceExpression("scenarioInfo"),
new CodeVariableReferenceExpression("ruleInfo"));

_codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression);

scenarioInitializeMethod.Statements.Add(expression);
}

private void SetupScenarioStartMethod(TestClassGenerationContext generationContext)
Expand Down
15 changes: 9 additions & 6 deletions Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,15 @@ internal void GenerateScenarioInitializeCall(TestClassGenerationContext generati

using (new SourceLineScope(_reqnrollConfiguration, _codeDomHelper, statements, generationContext.Document.SourceFilePath, scenario.Location))
{
statements.Add(new CodeExpressionStatement(
new CodeMethodInvokeExpression(
new CodeThisReferenceExpression(),
generationContext.ScenarioInitializeMethod.Name,
new CodeVariableReferenceExpression("scenarioInfo"),
new CodeVariableReferenceExpression("ruleInfo"))));
var callScenarioInitializeExpression = new CodeMethodInvokeExpression(
new CodeThisReferenceExpression(),
generationContext.ScenarioInitializeMethod.Name,
new CodeVariableReferenceExpression("scenarioInfo"),
new CodeVariableReferenceExpression("ruleInfo"));

_codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(callScenarioInitializeExpression);

statements.Add(new CodeExpressionStatement(callScenarioInitializeExpression));
}

testMethod.Statements.AddRange(statements.ToArray());
Expand Down
74 changes: 74 additions & 0 deletions Reqnroll.Tools.MsBuild.Generation/AsyncRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

#nullable enable

namespace Reqnroll.Tools.MsBuild.Generation;

internal static class AsyncRunner
{

#if NETFRAMEWORK
private static Func<object, Func<Task<T>>, T>? TryCreateRunDelegate<T>(object joinableTaskFactory)
{
// Find the Run<T> method
var runMethod = joinableTaskFactory.GetType().GetMethods()
.FirstOrDefault(m => m.Name == "Run" &&
m.IsGenericMethod &&
m.GetParameters().Length == 1 &&
typeof(Func<Task<T>>).IsAssignableFrom(m.GetParameters()[0].ParameterType.GetGenericArguments()[0]));

if (runMethod == null)
{
return null;
}

var genericRun = runMethod.MakeGenericMethod(typeof(T));

return (Func<object, Func<Task<T>>, T>)
genericRun.CreateDelegate(typeof(Func<object, Func<Task<T>>, T>));
}

#endif

/// <summary>
/// Runs an asynchronous function and blocks until it completes, returning the result.
/// </summary>
/// <typeparam name="T">The type returned by the function.</typeparam>
/// <param name="func">The function to invoke.</param>
/// <returns>The value returned by the function.</returns>
public static T RunAndJoin<T>(Func<Task<T>> func)
{
#if NETFRAMEWORK
// If we're running in Visual Studio, we want use its JoinableTaskFactory to avoid deadlocks.
// We can't guarantee the version of Visual Studio, so we use reflection to try to find
// ThreadHelper.JoinableTaskFactory at runtime
var threadHelperType = Type.GetType(
"Microsoft.VisualStudio.Shell.ThreadHelper, Microsoft.VisualStudio.Shell.15.0",
throwOnError: false);

if (threadHelperType != null)
{
var joinableTaskFactoryProperty = threadHelperType.GetProperty(
"JoinableTaskFactory",
BindingFlags.Public | BindingFlags.Static);

var joinableTaskFactory = joinableTaskFactoryProperty?.GetValue(null);
if (joinableTaskFactory != null)
{
var runDelegate = TryCreateRunDelegate<T>(joinableTaskFactory);
if (runDelegate != null)
{
return runDelegate(joinableTaskFactory, func);
}
}
}

#endif
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
return func().GetAwaiter().GetResult();
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace Reqnroll.Tools.MsBuild.Generation;

public class GenerateFeatureFileCodeBehindTask : Task
public class GenerateFeatureFileCodeBehindTask : Microsoft.Build.Utilities.Task
{
public const string CodeBehindFileMetadata = "CodeBehindFile"; //in
public const string MessagesFileMetadata = "MessagesFile"; //in,out
Expand Down Expand Up @@ -40,7 +41,9 @@ public class GenerateFeatureFileCodeBehindTask : Task

public bool LaunchDebugger { get; set; }

public override bool Execute()
public override bool Execute() => AsyncRunner.RunAndJoin(ExecuteAsync);

public async Task<bool> ExecuteAsync()
{
if (LaunchDebugger) Debugger.Launch();

Expand All @@ -55,20 +58,25 @@ public override bool Execute()
var reqnrollProjectInfo = new ReqnrollProjectInfo(generatorPlugins, featureFiles, ProjectPath, ProjectFolder, ProjectGuid, AssemblyName, OutputPath, RootNamespace, TargetFrameworks, TargetFramework);
var dependencyCustomizations = DependencyCustomizations ?? new NullGenerateFeatureFileCodeBehindTaskDependencyCustomizations();

using var taskRootContainer = generateFeatureFileCodeBehindTaskContainerBuilder.BuildRootContainer(Log, reqnrollProjectInfo, msbuildInformationProvider, dependencyCustomizations);
await using var taskRootContainer = generateFeatureFileCodeBehindTaskContainerBuilder.BuildRootContainer(
Log,
reqnrollProjectInfo,
msbuildInformationProvider,
dependencyCustomizations);

var assemblyResolveLoggerFactory = taskRootContainer.Resolve<IAssemblyResolveLoggerFactory>();

using (assemblyResolveLoggerFactory.Build())
{
var taskExecutor = taskRootContainer.Resolve<IGenerateFeatureFileCodeBehindTaskExecutor>();
var executeResult = taskExecutor.Execute();
var executeResult = await taskExecutor.ExecuteAsync();

if (executeResult is not ISuccess<IReadOnlyCollection<ITaskItem>> success)
{
return false;
}

GeneratedFiles = success.Result.ToArray();
GeneratedFiles = [.. success.Result];

return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Reqnroll.BoDi;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Reqnroll.BoDi;
using Reqnroll.CommonModels;
using Reqnroll.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Task = System.Threading.Tasks.Task;

namespace Reqnroll.Tools.MsBuild.Generation;
Expand All @@ -22,7 +23,7 @@ public class GenerateFeatureFileCodeBehindTaskExecutor(
IExceptionTaskLogger exceptionTaskLogger)
: IGenerateFeatureFileCodeBehindTaskExecutor
{
public IResult<IReadOnlyCollection<ITaskItem>> Execute()
public async Task<IResult<IReadOnlyCollection<ITaskItem>>> ExecuteAsync()
{
processInfoDumper.DumpProcessInfo();
log.LogTaskMessage("Starting GenerateFeatureFileCodeBehind task");
Expand All @@ -31,7 +32,7 @@ public IResult<IReadOnlyCollection<ITaskItem>> Execute()
{
var reqnrollProject = reqnrollProjectProvider.GetReqnrollProject();

using var generatorContainer = wrappedGeneratorContainerBuilder.BuildGeneratorContainer(
await using var generatorContainer = wrappedGeneratorContainerBuilder.BuildGeneratorContainer(
reqnrollProject.ProjectSettings.ConfigurationHolder,
reqnrollProject.ProjectSettings,
reqnrollProjectInfo.GeneratorPlugins,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.Collections.Generic;
using Microsoft.Build.Framework;
using Reqnroll.CommonModels;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Reqnroll.Tools.MsBuild.Generation
{
public interface IGenerateFeatureFileCodeBehindTaskExecutor
{
IResult<IReadOnlyCollection<ITaskItem>> Execute();
Task<IResult<IReadOnlyCollection<ITaskItem>>> ExecuteAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
<Reference Include="Microsoft.Build.Utilities.Core">
<HintPath>Microsoft.Build.Utilities.Core</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Composition" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Reqnroll/BoDi/IObjectContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Reqnroll.BoDi;

public interface IObjectContainer : IDisposable
public interface IObjectContainer : IAsyncDisposable
{
/// <summary>
/// Fired when a new object is created directly by the container. It is not invoked for resolving instance and factory registrations.
Expand Down
23 changes: 20 additions & 3 deletions Reqnroll/BoDi/ObjectContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Reqnroll.BoDi;

Expand Down Expand Up @@ -744,15 +745,31 @@ private void AssertNotDisposed()
throw new ObjectContainerException("Object container disposed", null);
}

public void Dispose()
public async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}

_isDisposed = true;

foreach (var obj in _objectPool.Values.OfType<IDisposable>().Where(o => !ReferenceEquals(o, this)))
obj.Dispose();
foreach (var obj in _objectPool.Values)
{
if (ReferenceEquals(obj, this))
{
continue;
}

if (obj is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else if (obj is IDisposable disposable)
{
disposable.Dispose();
}
}

_objectPool.Clear();
_registrations.Clear();
Expand Down
2 changes: 1 addition & 1 deletion Reqnroll/ITestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public interface ITestRunner
Task OnFeatureStartAsync(FeatureInfo featureInfo);
Task OnFeatureEndAsync();

void OnScenarioInitialize(ScenarioInfo scenarioInfo, RuleInfo ruleInfo);
Task OnScenarioInitializeAsync(ScenarioInfo scenarioInfo, RuleInfo ruleInfo);
Task OnScenarioStartAsync();

Task CollectScenarioErrorsAsync();
Expand Down
Loading
Loading