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
2 changes: 2 additions & 0 deletions TUnit.Analyzers.Tests/DataSourceGeneratorAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ public class CustomDataAttribute<T> : Attribute, IDataSourceAttribute
{
public bool SkipIfEmpty { get; set; }

public bool DeferEnumeration { get; set; }

public async IAsyncEnumerable<Func<Task<object?[]?>>> GetDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
// Returns Foo instances as the test expects, not T
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// <auto-generated/>
#pragma warning disable

#nullable enable
namespace TUnit.Generated;
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("TUnit", "VERSION_SCRUBBED")]
internal static class TUnit_TestProject_DeferEnumerationTests_DeferEnumerationTests__TestSource
{
private static readonly global::TUnit.Core.ClassMetadata __classMetadata = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests", new global::TUnit.Core.ClassMetadata
{
Type = typeof(global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests),
TypeInfo = new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests)),
Name = "DeferEnumerationTests",
Namespace = "TUnit.TestProject.DeferEnumerationTests",
Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", "TestsBase`1"),
Parameters = global::System.Array.Empty<global::TUnit.Core.ParameterMetadata>(),
Properties = global::System.Array.Empty<global::TUnit.Core.PropertyMetadata>(),
Parent = null
});
private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests);
private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Deferred", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests).GetMethod("Deferred", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(int) }, null)!.GetParameters()[0])
});
private static readonly global::TUnit.Core.MethodMetadata __mm_1 = global::TUnit.Core.MethodMetadataFactory.Create("DeferredWithRepeat", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, parameters: new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false, reflectionInfoFactory: static () => typeof(global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests).GetMethod("DeferredWithRepeat", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { typeof(int) }, null)!.GetParameters()[0])
});
private static global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests __CreateInstance(global::System.Type[] typeArgs, object?[] args)
{
return new global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests();
}
private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken)
{
switch (methodIndex)
{
case 0:
{
try
{
switch (args.Length)
{
case 1:
{
return new global::System.Threading.Tasks.ValueTask(instance.Deferred(global::TUnit.Core.Helpers.CastHelper.Cast<int>(args[0])));
}
default:
throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
}
}
catch (global::System.Exception ex)
{
return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex));
}
}
case 1:
{
try
{
switch (args.Length)
{
case 1:
{
return new global::System.Threading.Tasks.ValueTask(instance.DeferredWithRepeat(global::TUnit.Core.Helpers.CastHelper.Cast<int>(args[0])));
}
default:
throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
}
}
catch (global::System.Exception ex)
{
return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex));
}
}
default:
throw new global::System.ArgumentOutOfRangeException(nameof(methodIndex));
}
}
private static global::System.Attribute[] __Attributes(int groupIndex)
{
switch (groupIndex)
{
case 0:
{
return
[
new global::TUnit.Core.TestAttribute()
];
}
case 1:
{
return
[
new global::TUnit.Core.TestAttribute(),
new global::TUnit.Core.RepeatAttribute(2)
];
}
default:
throw new global::System.ArgumentOutOfRangeException(nameof(groupIndex));
}
}
public static readonly global::TUnit.Core.TestEntry<global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests>[] Entries = new global::TUnit.Core.TestEntry<global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests>[]
{
new global::TUnit.Core.TestEntry<global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests>
{
MethodName = "Deferred",
FullyQualifiedName = "TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests.Deferred",
FilePath = "",
LineNumber = 9,
Categories = global::System.Array.Empty<string>(),
Properties = global::System.Array.Empty<string>(),
HasDataSource = true,
RepeatCount = 0,
DependsOn = global::System.Array.Empty<string>(),
MethodMetadata = __mm_0,
CreateInstance = __CreateInstance,
InvokeBody = __Invoke,
MethodIndex = 0,
CreateAttributes = __Attributes,
AttributeGroupIndex = 0,
TestDataSources = new global::TUnit.Core.IDataSourceAttribute[]
{
new global::TUnit.Core.MethodDataSourceAttribute("TenValues")
{
DeferEnumeration = true,
Factory = (dataGeneratorMetadata) =>
{
async global::System.Collections.Generic.IAsyncEnumerable<global::System.Func<global::System.Threading.Tasks.Task<object?[]?>>> Factory()
{
var result = global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests.TenValues();
if (result is global::System.Collections.IEnumerable enumerable && !(result is string))
{
foreach (var item in enumerable)
{
yield return () => global::System.Threading.Tasks.Task.FromResult(global::TUnit.Core.Helpers.DataSourceHelpers.ToObjectArray(item));
}
}
else
{
yield return () => global::System.Threading.Tasks.Task.FromResult(global::TUnit.Core.Helpers.DataSourceHelpers.ToObjectArray(result));
}
}
return Factory();
}
},
},
},
new global::TUnit.Core.TestEntry<global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests>
{
MethodName = "DeferredWithRepeat",
FullyQualifiedName = "TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests.DeferredWithRepeat",
FilePath = "",
LineNumber = 16,
Categories = global::System.Array.Empty<string>(),
Properties = global::System.Array.Empty<string>(),
HasDataSource = true,
RepeatCount = 2,
DependsOn = global::System.Array.Empty<string>(),
MethodMetadata = __mm_1,
CreateInstance = __CreateInstance,
InvokeBody = __Invoke,
MethodIndex = 1,
CreateAttributes = __Attributes,
AttributeGroupIndex = 1,
TestDataSources = new global::TUnit.Core.IDataSourceAttribute[]
{
new global::TUnit.Core.MethodDataSourceAttribute("TenValues")
{
DeferEnumeration = true,
Factory = (dataGeneratorMetadata) =>
{
async global::System.Collections.Generic.IAsyncEnumerable<global::System.Func<global::System.Threading.Tasks.Task<object?[]?>>> Factory()
{
var result = global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests.TenValues();
if (result is global::System.Collections.IEnumerable enumerable && !(result is string))
{
foreach (var item in enumerable)
{
yield return () => global::System.Threading.Tasks.Task.FromResult(global::TUnit.Core.Helpers.DataSourceHelpers.ToObjectArray(item));
}
}
else
{
yield return () => global::System.Threading.Tasks.Task.FromResult(global::TUnit.Core.Helpers.DataSourceHelpers.ToObjectArray(result));
}
}
return Factory();
}
},
},
},
};
}
internal static partial class TUnit_TestRegistration
{
static readonly int _r_TUnit_TestProject_DeferEnumerationTests_DeferEnumerationTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries<global::TUnit.TestProject.DeferEnumerationTests.DeferEnumerationTests>(static () => TUnit_TestProject_DeferEnumerationTests_DeferEnumerationTests__TestSource.Entries);
}
16 changes: 16 additions & 0 deletions TUnit.Core.SourceGenerator.Tests/DeferEnumerationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

namespace TUnit.Core.SourceGenerator.Tests;

internal class DeferEnumerationTests : TestsBase
{
[Test]
public Task Test() => RunTest(Path.Combine(Git.RootDirectory.FullName,
"TUnit.TestProject",
"DeferEnumerationTests",
"DeferEnumerationTests.cs"),
async generatedFiles =>
{
await Assert.That(generatedFiles).IsNotEmpty();
await Assert.That(string.Join("\n", generatedFiles)).Contains("DeferEnumeration = true");
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,15 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu
writer.AppendLine(",");
}

// Copy over DeferEnumeration if set so the test is enumerated at runtime instead of discovery.
// The hand-built initializer above only copies the named args it knows about, so this must be
// threaded explicitly (unlike the method-not-found path which round-trips all named arguments).
var deferProperty = attr.NamedArguments.FirstOrDefault(x => x.Key == "DeferEnumeration");
if (deferProperty is { Key: not null, Value.IsNull: false } && deferProperty.Value.Value is true)
{
writer.AppendLine("DeferEnumeration = true,");
}

// Set the Factory property with a strongly-typed function
writer.AppendLine("Factory = (dataGeneratorMetadata) =>");
writer.AppendLine("{");
Expand Down
8 changes: 8 additions & 0 deletions TUnit.Core/Attributes/TestData/ArgumentsAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public sealed class ArgumentsAttribute : Attribute, IDataSourceAttribute, ITestR
/// <inheritdoc />
public bool SkipIfEmpty { get; set; }

/// <inheritdoc />
// [Arguments] yields a single row, so deferring its enumeration would be pure overhead - always false.
public bool DeferEnumeration { get => false; set { } }

/// <summary>
/// Initializes a new instance of the <see cref="ArgumentsAttribute"/> class with the specified test argument values.
/// </summary>
Expand Down Expand Up @@ -166,6 +170,10 @@ public sealed class ArgumentsAttribute<T>(T value) : TypedDataSourceAttribute<T>
/// <inheritdoc />
public override bool SkipIfEmpty { get; set; }

/// <inheritdoc />
// [Arguments] yields a single row, so deferring its enumeration would be pure overhead - always false.
public override bool DeferEnumeration { get => false; set { } }

public override async IAsyncEnumerable<Func<Task<T>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
yield return () => Task.FromResult(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ public abstract class AsyncUntypedDataSourceGeneratorAttribute : Attribute, IAsy
/// <inheritdoc />
public virtual bool SkipIfEmpty { get; set; }

/// <inheritdoc />
public virtual bool DeferEnumeration { get; set; }

protected abstract IAsyncEnumerable<Func<Task<object?[]?>>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata);

public IAsyncEnumerable<Func<Task<object?[]?>>> GenerateAsync(DataGeneratorMetadata dataGeneratorMetadata)
Expand Down
3 changes: 3 additions & 0 deletions TUnit.Core/Attributes/TestData/DelegateDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ internal sealed class DelegateDataSourceAttribute : Attribute, IDataSourceAttrib
/// <inheritdoc />
public bool SkipIfEmpty { get; set; }

/// <inheritdoc />
public bool DeferEnumeration { get; set; }

public DelegateDataSourceAttribute(Func<DataGeneratorMetadata, IAsyncEnumerable<object?[]>> factory, bool isShared = false)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
Expand Down
4 changes: 4 additions & 0 deletions TUnit.Core/Attributes/TestData/EmptyDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ internal sealed class EmptyDataSourceAttribute : Attribute, IDataSourceAttribute
/// <inheritdoc />
public bool SkipIfEmpty { get; set; }

/// <inheritdoc />
// Always a single (empty) row, so deferring its enumeration would be pure overhead.
public bool DeferEnumeration { get => false; set { } }

public async IAsyncEnumerable<Func<Task<object?[]?>>> GetDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
yield return () => Task.FromResult<object?[]?>([
Expand Down
15 changes: 15 additions & 0 deletions TUnit.Core/Attributes/TestData/IDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,19 @@ public interface IDataSourceAttribute
/// When true, if the data source returns no data, the test will be skipped instead of failing.
/// </summary>
bool SkipIfEmpty { get; set; }

/// <summary>
/// When <c>true</c>, this data source is not enumerated during test discovery. Instead, the test
/// appears as a single placeholder node, and the data rows are enumerated and executed at runtime
/// (each reported as a result nested under the placeholder). This avoids the IDE/test-explorer
/// overhead of expanding a data source that produces a large number of cases.
/// </summary>
/// <remarks>
/// If any data source on a test sets this to <c>true</c>, the entire test's case expansion is
/// deferred to runtime. Tests deferred this way cannot be targeted individually by a filter, and
/// other tests cannot <c>[DependsOn]</c> their rows (the rows do not exist until runtime).
/// Single-row sources (such as <c>[Arguments]</c>) ignore this flag — there is nothing to defer —
/// so setting it on them has no effect.
/// </remarks>
bool DeferEnumeration { get; set; }
}
4 changes: 4 additions & 0 deletions TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public class MethodDataSourceAttribute : Attribute, IDataSourceAttribute
/// <inheritdoc />
public bool SkipIfEmpty { get; set; }

/// <inheritdoc />
public bool DeferEnumeration { get; set; }

public MethodDataSourceAttribute(string methodNameProvidingDataSource)
{
if (methodNameProvidingDataSource is null or { Length: < 1 })
Expand Down Expand Up @@ -143,6 +146,7 @@ internal InstanceMethodDataSourceAttribute ToInstanceVariant()

converted.Arguments = Arguments;
converted.SkipIfEmpty = SkipIfEmpty;
converted.DeferEnumeration = DeferEnumeration;

return converted;
}
Expand Down
3 changes: 3 additions & 0 deletions TUnit.Core/Attributes/TestData/NoDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ internal class NoDataSource : IDataSourceAttribute
/// <inheritdoc />
public bool SkipIfEmpty { get; set; }

/// <inheritdoc />
public bool DeferEnumeration { get; set; }

public async IAsyncEnumerable<Func<Task<object?[]?>>> GetDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
{
yield return static () => _emptyRowTask;
Expand Down
3 changes: 3 additions & 0 deletions TUnit.Core/Attributes/TestData/StaticDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ internal sealed class StaticDataSourceAttribute : Attribute, IDataSourceAttribut
/// <inheritdoc />
public bool SkipIfEmpty { get; set; }

/// <inheritdoc />
public bool DeferEnumeration { get; set; }

public StaticDataSourceAttribute(params object?[][] data)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
Expand Down
3 changes: 3 additions & 0 deletions TUnit.Core/Attributes/TestData/TypedDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ public abstract class TypedDataSourceAttribute<T> : Attribute, ITypedDataSourceA
/// <inheritdoc />
public virtual bool SkipIfEmpty { get; set; }

/// <inheritdoc />
public virtual bool DeferEnumeration { get; set; }

public abstract IAsyncEnumerable<Func<Task<T>>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata);

public async IAsyncEnumerable<Func<Task<object?[]?>>> GetDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
Expand Down
Loading
Loading