Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions TUnit.Assertions/Sources/DelegateAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ public DelegateAssertion(Action action, string? expression)
Action = action ?? throw new ArgumentNullException(nameof(action));
var expressionBuilder = new StringBuilder();
expressionBuilder.Append($"Assert.That({expression ?? "?"})");
var evaluationContext = new EvaluationContext<object?>(async () =>
var evaluationContext = new EvaluationContext<object?>(() =>
{
try
{
action();
return (null, null);
return Task.FromResult<(object?, Exception?)>((null, null));
}
catch (Exception ex)
{
return (null, ex);
return Task.FromResult<(object?, Exception?)>((null, ex));
}
});
Context = new AssertionContext<object?>(evaluationContext, expressionBuilder);
Expand Down
6 changes: 3 additions & 3 deletions TUnit.Assertions/Sources/FuncAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ public FuncAssertion(Func<TValue?> func, string? expression)
{
var expressionBuilder = new StringBuilder();
expressionBuilder.Append($"Assert.That({expression ?? "?"})");
var evaluationContext = new EvaluationContext<TValue>(async () =>
var evaluationContext = new EvaluationContext<TValue>(() =>
{
try
{
var result = func();
return (result, null);
return Task.FromResult<(TValue?, Exception?)>((result, null));
}
catch (Exception ex)
{
return (default(TValue), ex);
return Task.FromResult<(TValue?, Exception?)>((default(TValue), ex));
}
});
Context = new AssertionContext<TValue>(evaluationContext, expressionBuilder);
Expand Down
43 changes: 43 additions & 0 deletions TUnit.Core/Attributes/ReflectionModeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace TUnit.Core;

/// <summary>
/// Attribute that forces the test assembly to use reflection mode for test discovery and execution.
/// </summary>
/// <remarks>
/// <para>
/// Use this attribute when source generation cannot be used for test discovery, such as when
/// working with dynamically generated types (e.g., Razor components in bUnit tests).
/// </para>
///
/// <para>
/// This attribute should be applied at the assembly level and affects all tests in the assembly.
/// Command-line options (--reflection) can still override this setting.
/// </para>
///
/// <para>
/// <strong>Performance Note:</strong> Reflection mode is slower than source-generated mode.
/// Only use this attribute when source generation is incompatible with your test scenarios.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // Add to your test project (e.g., in AssemblyInfo.cs or at the top of any .cs file)
/// using TUnit.Core;
///
/// [assembly: ReflectionMode]
///
/// // All tests in this assembly will now use reflection mode
/// public class MyBunitTests
/// {
/// [Test]
/// public void TestRazorComponent()
/// {
/// // Test Razor components that are source-generated at compile time
/// }
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
public sealed class ReflectionModeAttribute : Attribute
{
}
47 changes: 1 addition & 46 deletions TUnit.Engine/Framework/TUnitServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public TUnitServiceProvider(IExtension extension,
var logLevelProvider = Register(new LogLevelProvider(CommandLineOptions));

// Determine execution mode early to create appropriate services
var useSourceGeneration = SourceRegistrar.IsEnabled = GetUseSourceGeneration(CommandLineOptions);
var useSourceGeneration = SourceRegistrar.IsEnabled = ExecutionModeHelper.IsSourceGenerationMode(CommandLineOptions);

// Create and register mode-specific hook discovery service
IHookDiscoveryService hookDiscoveryService;
Expand Down Expand Up @@ -275,51 +275,6 @@ private T Register<T>(T service) where T : class
return service;
}

private static bool GetUseSourceGeneration(ICommandLineOptions commandLineOptions)
{
#if NET
if (!RuntimeFeature.IsDynamicCodeSupported)
{
return true; // Force source generation on AOT platforms
}
#endif

if (commandLineOptions.TryGetOptionArgumentList(ReflectionModeCommandProvider.ReflectionMode, out _))
{
return false; // Reflection mode explicitly requested
}

// Check for command line option
if (commandLineOptions.TryGetOptionArgumentList("tunit-execution-mode", out var modes) && modes.Length > 0)
{
var mode = modes[0].ToLowerInvariant();
if (mode == "sourcegeneration" || mode == "aot")
{
return true;
}
else if (mode == "reflection")
{
return false;
}
}

// Check environment variable
var envMode = EnvironmentVariableCache.Get("TUNIT_EXECUTION_MODE");
if (!string.IsNullOrEmpty(envMode))
{
var mode = envMode!.ToLowerInvariant();
if (mode == "sourcegeneration" || mode == "aot")
{
return true;
}
else if (mode == "reflection")
{
return false;
}
}

return SourceRegistrar.IsEnabled;
}

public async ValueTask DisposeAsync()
{
Expand Down
86 changes: 86 additions & 0 deletions TUnit.Engine/Helpers/ExecutionModeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Testing.Platform.CommandLine;
using TUnit.Core;
using TUnit.Engine.CommandLineProviders;

namespace TUnit.Engine.Helpers;

/// <summary>
/// Helper class for determining test execution mode (source generation vs reflection).
/// </summary>
internal static class ExecutionModeHelper
{
/// <summary>
/// Determines whether to use source generation mode for test discovery and execution.
/// </summary>
/// <param name="commandLineOptions">Command line options from the test platform.</param>
/// <returns>
/// True if source generation mode should be used; false if reflection mode should be used.
/// </returns>
/// <remarks>
/// Priority order:
/// 1. AOT platform check (forces source generation)
/// 2. Command line --reflection flag
/// 3. Command line --tunit-execution-mode
/// 4. Assembly-level [ReflectionMode] attribute
/// 5. Environment variable TUNIT_EXECUTION_MODE
/// 6. Default (SourceRegistrar.IsEnabled)
/// </remarks>
public static bool IsSourceGenerationMode(ICommandLineOptions commandLineOptions)
{
#if NET
if (!RuntimeFeature.IsDynamicCodeSupported)
{
return true; // Force source generation on AOT platforms
}
#endif

if (commandLineOptions.TryGetOptionArgumentList(ReflectionModeCommandProvider.ReflectionMode, out _))
{
return false; // Reflection mode explicitly requested
}

// Check for command line option
if (commandLineOptions.TryGetOptionArgumentList("tunit-execution-mode", out var modes) && modes.Length > 0)
{
var mode = modes[0].ToLowerInvariant();
if (mode == "sourcegeneration" || mode == "aot")
{
return true;
}
else if (mode == "reflection")
{
return false;
}
}

// Check for assembly-level ReflectionMode attribute
var entryAssembly = Assembly.GetEntryAssembly();
if (entryAssembly != null)
{
var hasReflectionModeAttribute = entryAssembly.GetCustomAttributes(typeof(ReflectionModeAttribute), inherit: false).Length > 0;
if (hasReflectionModeAttribute)
{
return false; // Assembly is marked for reflection mode
}
}

// Check environment variable
var envMode = EnvironmentVariableCache.Get("TUNIT_EXECUTION_MODE");
if (!string.IsNullOrEmpty(envMode))
{
var mode = envMode!.ToLowerInvariant();
if (mode == "sourcegeneration" || mode == "aot")
{
return true;
}
else if (mode == "reflection")
{
return false;
}
}

return SourceRegistrar.IsEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,11 @@ namespace
public static ..IPropertySource? GetSource( type) { }
public static void Register( type, ..IPropertySource source) { }
}
[(.Assembly, AllowMultiple=false, Inherited=false)]
public sealed class ReflectionModeAttribute :
{
public ReflectionModeAttribute() { }
}
[(.Assembly | .Class | .Method)]
public sealed class RepeatAttribute : .TUnitAttribute, .IScopedAttribute
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,11 @@ namespace
public static ..IPropertySource? GetSource( type) { }
public static void Register( type, ..IPropertySource source) { }
}
[(.Assembly, AllowMultiple=false, Inherited=false)]
public sealed class ReflectionModeAttribute :
{
public ReflectionModeAttribute() { }
}
[(.Assembly | .Class | .Method)]
public sealed class RepeatAttribute : .TUnitAttribute, .IScopedAttribute
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,11 @@ namespace
public static ..IPropertySource? GetSource( type) { }
public static void Register( type, ..IPropertySource source) { }
}
[(.Assembly, AllowMultiple=false, Inherited=false)]
public sealed class ReflectionModeAttribute :
{
public ReflectionModeAttribute() { }
}
[(.Assembly | .Class | .Method)]
public sealed class RepeatAttribute : .TUnitAttribute, .IScopedAttribute
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,11 @@ namespace
public static ..IPropertySource? GetSource( type) { }
public static void Register( type, ..IPropertySource source) { }
}
[(.Assembly, AllowMultiple=false, Inherited=false)]
public sealed class ReflectionModeAttribute :
{
public ReflectionModeAttribute() { }
}
[(.Assembly | .Class | .Method)]
public sealed class RepeatAttribute : .TUnitAttribute, .IScopedAttribute
{
Expand Down
59 changes: 55 additions & 4 deletions docs/docs/execution/engine-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,70 @@ This is the standard mode used for all builds, whether debugging, running tests,

## Reflection Mode

Reflection mode can be explicitly enabled using the `--reflection` command-line flag:
Reflection mode can be explicitly enabled in several ways:

- **Runtime Discovery**: Tests are discovered at runtime using reflection
- **Dynamic Execution**: Uses traditional reflection-based test invocation
- **Compatibility**: Useful for scenarios where source generation may not be suitable
- **Compatibility**: Useful for scenarios where source generation may not be suitable (e.g., bUnit with Razor components)
- **Legacy Support**: Maintains compatibility with reflection-dependent test patterns

Enable reflection mode by running:
### Enabling Reflection Mode

There are three ways to enable reflection mode, listed in priority order:

#### 1. Command-Line Flag (Highest Priority)
```bash
dotnet test -- --reflection
```

Alternatively, setting the environment variable `TUNIT_EXECUTION_MODE` to `reflection` enables the reflection engine mode globally.
#### 2. Assembly Attribute (Recommended for Per-Project Configuration)
Add to any `.cs` file in your test project (e.g., `AssemblyInfo.cs`):
```csharp
using TUnit.Core;

[assembly: ReflectionMode]
```

This is the recommended approach when you need reflection mode for a specific test assembly, such as bUnit projects that test Razor components. The configuration is version-controlled and doesn't require external configuration files.

**Example: bUnit Test Project**
```csharp
// Add this to enable reflection mode for your bUnit tests
[assembly: ReflectionMode]

namespace MyApp.Tests;

public class CounterComponentTests : TestContext
{
[Test]
public void CounterStartsAtZero()
{
// Test Razor components that are source-generated at compile time
var cut = RenderComponent<Counter>();
cut.Find("p").TextContent.ShouldBe("Current count: 0");
}
}
```

#### 3. Environment Variable (Global Configuration)
```bash
# Windows
set TUNIT_EXECUTION_MODE=reflection

# Linux/macOS
export TUNIT_EXECUTION_MODE=reflection
```

Alternatively, you can configure this in a `.runsettings` file:
```xml
<RunSettings>
<RunConfiguration>
<EnvironmentVariables>
<TUNIT_EXECUTION_MODE>reflection</TUNIT_EXECUTION_MODE>
</EnvironmentVariables>
</RunConfiguration>
</RunSettings>
```

## Native AOT Support

Expand Down
Loading