diff --git a/TUnit.Analyzers.Tests/CombinedDataSourceAnalyzerTests.cs b/TUnit.Analyzers.Tests/CombinedDataSourceAnalyzerTests.cs index 6e6055cd77..88f6285e0b 100644 --- a/TUnit.Analyzers.Tests/CombinedDataSourceAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/CombinedDataSourceAnalyzerTests.cs @@ -14,7 +14,7 @@ await Verifier public class MyClass { - [CombinedDataSource] + [CombinedDataSources] [Test] public void MyTest( [Arguments(1, 2, 3)] int value, @@ -61,7 +61,7 @@ await Verifier public class MyClass { - [CombinedDataSource] + [CombinedDataSources] [Test] public void MyTest( [Arguments(1, 2, 3)] int value, @@ -87,7 +87,7 @@ await Verifier public class MyClass { - [CombinedDataSource] + [CombinedDataSources] [MatrixDataSource] [Test] public void {|#0:MyTest|}( @@ -111,7 +111,7 @@ await Verifier """ using TUnit.Core; - [CombinedDataSource] + [CombinedDataSources] public class MyClass { public MyClass( @@ -169,7 +169,7 @@ await Verifier public class MyClass { - [CombinedDataSource] + [CombinedDataSources] [Test] public void MyTest( [Arguments(1, 2, 3)] int value, @@ -195,7 +195,7 @@ public class MyClass { public static IEnumerable GetNumbers() => [1, 2, 3]; - [CombinedDataSource] + [CombinedDataSources] [Test] public void MyTest( [MethodDataSource(nameof(GetNumbers))] int number, @@ -229,7 +229,7 @@ protected override IEnumerable> GenerateDataSources(DataGeneratorMetad public class MyClass { - [CombinedDataSource] + [CombinedDataSources] [Test] public void MyTest( [ClassDataSource] int number, @@ -252,7 +252,7 @@ await Verifier public class MyClass { - [CombinedDataSource] + [CombinedDataSources] [Test] public void MyTest( [Arguments(1, 2, 3)] int value, @@ -300,7 +300,7 @@ await Verifier """ using TUnit.Core; - [CombinedDataSource] + [CombinedDataSources] public class MyClass { public MyClass( diff --git a/TUnit.Analyzers/CombinedDataSourceAnalyzer.cs b/TUnit.Analyzers/CombinedDataSourceAnalyzer.cs index c9d1c3b61c..2c50620686 100644 --- a/TUnit.Analyzers/CombinedDataSourceAnalyzer.cs +++ b/TUnit.Analyzers/CombinedDataSourceAnalyzer.cs @@ -6,7 +6,7 @@ namespace TUnit.Analyzers; /// -/// Analyzer for CombinedDataSource validation +/// Analyzer for CombinedDataSources validation /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public class CombinedDataSourceAnalyzer : ConcurrentDiagnosticAnalyzer @@ -66,7 +66,7 @@ private void CheckCombinedDataSourceErrors(SymbolAnalysisContext context, .Where(p => p.HasDataSourceAttribute(context.Compilation)) .ToList(); - // Rule 1: If parameters have data source attributes, CombinedDataSource must be present + // Rule 1: If parameters have data source attributes, CombinedDataSources must be present if (parametersWithDataSources.Any() && !hasCombinedDataSource) { context.ReportDiagnostic( @@ -75,7 +75,7 @@ private void CheckCombinedDataSourceErrors(SymbolAnalysisContext context, ); } - // Rule 2: If CombinedDataSource is present, all parameters must have data sources + // Rule 2: If CombinedDataSources is present, all parameters must have data sources if (hasCombinedDataSource) { // Filter out CancellationToken parameters as they're handled by the engine @@ -96,7 +96,7 @@ private void CheckCombinedDataSourceErrors(SymbolAnalysisContext context, } } - // Rule 3: Warn if mixing CombinedDataSource with MatrixDataSource + // Rule 3: Warn if mixing CombinedDataSources with MatrixDataSource var hasMatrixDataSource = attributes.Any(x => x.IsMatrixDataSourceAttribute(context.Compilation)); if (hasMatrixDataSource) { diff --git a/TUnit.Analyzers/Helpers/WellKnown.cs b/TUnit.Analyzers/Helpers/WellKnown.cs index ac315237de..a2d9e32696 100644 --- a/TUnit.Analyzers/Helpers/WellKnown.cs +++ b/TUnit.Analyzers/Helpers/WellKnown.cs @@ -8,7 +8,7 @@ public static class AttributeFullyQualifiedClasses public static readonly FullyQualifiedTypeName Explicit = GetTypeName("ExplicitAttribute"); public static readonly FullyQualifiedTypeName Matrix = GetTypeName("MatrixAttribute"); public static readonly FullyQualifiedTypeName MatrixDataSourceAttribute = GetTypeName("MatrixDataSourceAttribute"); - public static readonly FullyQualifiedTypeName CombinedDataSourceAttribute = GetTypeName("CombinedDataSourceAttribute"); + public static readonly FullyQualifiedTypeName CombinedDataSourceAttribute = GetTypeName("CombinedDataSourcesAttribute"); public static readonly FullyQualifiedTypeName BeforeAttribute = GetTypeName("BeforeAttribute"); public static readonly FullyQualifiedTypeName AfterAttribute = GetTypeName("AfterAttribute"); diff --git a/TUnit.Analyzers/Resources.resx b/TUnit.Analyzers/Resources.resx index 8f9e8bdf03..b68d6eadff 100644 --- a/TUnit.Analyzers/Resources.resx +++ b/TUnit.Analyzers/Resources.resx @@ -463,28 +463,28 @@ Data source may produce no tests - When parameters have data source attributes, the method or class must be marked with [CombinedDataSource] to combine the parameter data sources. + When parameters have data source attributes, the method or class must be marked with [CombinedDataSources] to combine the parameter data sources. - [CombinedDataSource] is required when parameters have data source attributes. + [CombinedDataSources] is required when parameters have data source attributes. - [CombinedDataSource] attribute required + [CombinedDataSources] attribute required - When [CombinedDataSource] is used, all parameters (except CancellationToken) must have data source attributes to provide test data. + When [CombinedDataSources] is used, all parameters (except CancellationToken) must have data source attributes to provide test data. - Parameter '{0}' is missing a data source attribute. All parameters must have data sources when using [CombinedDataSource]. + Parameter '{0}' is missing a data source attribute. All parameters must have data sources when using [CombinedDataSources]. Parameter missing data source attribute - Using [CombinedDataSource] together with [MatrixDataSource] is not recommended as they serve different purposes and may cause confusion. + Using [CombinedDataSources] together with [MatrixDataSource] is not recommended as they serve different purposes and may cause confusion. - [CombinedDataSource] should not be used with [MatrixDataSource]. Use one or the other. + [CombinedDataSources] should not be used with [MatrixDataSource]. Use one or the other. Conflicting data source attributes diff --git a/TUnit.Assertions/AssertionScope.cs b/TUnit.Assertions/AssertionScope.cs index d3d2a410a0..618b865d6b 100644 --- a/TUnit.Assertions/AssertionScope.cs +++ b/TUnit.Assertions/AssertionScope.cs @@ -1,4 +1,5 @@ using System.Runtime.ExceptionServices; +using System.Text; using TUnit.Assertions.Exceptions; namespace TUnit.Assertions; @@ -44,7 +45,17 @@ public void Dispose() ExceptionDispatchInfo.Capture(_exceptions[0]).Throw(); } - var message = string.Join(Environment.NewLine + Environment.NewLine, _exceptions.Select(e => e.Message)); + // Use StringBuilder for message concatenation instead of LINQ + var sb = new StringBuilder(); + for (int i = 0; i < _exceptions.Count; i++) + { + if (i > 0) + { + sb.Append(Environment.NewLine).Append(Environment.NewLine); + } + sb.Append(_exceptions[i].Message); + } + var message = sb.ToString(); throw new AssertionException(message, new AggregateException(_exceptions)); } diff --git a/TUnit.Assertions/Conditions/StringAssertions.cs b/TUnit.Assertions/Conditions/StringAssertions.cs index 95e20d0ed1..0538f96796 100644 --- a/TUnit.Assertions/Conditions/StringAssertions.cs +++ b/TUnit.Assertions/Conditions/StringAssertions.cs @@ -108,8 +108,10 @@ private static string RemoveWhitespace(string input) return input; } +#if NETSTANDARD2_0 + // Use StringBuilder for netstandard2.0 compatibility var sb = new StringBuilder(input.Length); - foreach (var c in input) + foreach (char c in input) { if (!char.IsWhiteSpace(c)) { @@ -117,6 +119,23 @@ private static string RemoveWhitespace(string input) } } return sb.ToString(); +#else + // Use Span for better performance on modern .NET + Span buffer = input.Length <= 256 + ? stackalloc char[input.Length] + : new char[input.Length]; + + int writeIndex = 0; + foreach (char c in input) + { + if (!char.IsWhiteSpace(c)) + { + buffer[writeIndex++] = c; + } + } + + return new string(buffer.Slice(0, writeIndex)); +#endif } protected override string GetExpectation() => $"to contain \"{_expected}\""; diff --git a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs index 0c29255d19..99ee7e6af4 100644 --- a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs +++ b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs @@ -101,8 +101,8 @@ protected override Task CheckAsync(EvaluationMetadata m if (_ignoringWhitespace) { - actualValue = actualValue != null ? string.Concat(actualValue.Where(c => !char.IsWhiteSpace(c))) : null; - expectedValue = expectedValue != null ? string.Concat(expectedValue.Where(c => !char.IsWhiteSpace(c))) : null; + actualValue = actualValue != null ? RemoveWhitespace(actualValue) : null; + expectedValue = expectedValue != null ? RemoveWhitespace(expectedValue) : null; } if (_nullAndEmptyEquality) @@ -165,16 +165,28 @@ private string BuildStringDifferenceMessage(string? originalValue, string? actua int contextEnd = Math.Min(expectedValue.Length, Math.Min(actualValue.Length, diffIndex + 27)); // Build the diff display with arrows +#if NET8_0_OR_GREATER + var expectedContextLength = Math.Min(contextEnd - contextStart, expectedValue.Length - contextStart); + var actualContextLength = Math.Min(contextEnd - contextStart, actualValue.Length - contextStart); + var expectedContext = expectedValue.AsSpan(contextStart, expectedContextLength); + var actualContext = actualValue.AsSpan(contextStart, actualContextLength); +#else var expectedContext = expectedValue.Substring(contextStart, Math.Min(contextEnd - contextStart, expectedValue.Length - contextStart)); var actualContext = actualValue.Substring(contextStart, Math.Min(contextEnd - contextStart, actualValue.Length - contextStart)); +#endif // Calculate arrow position (relative to context start + prefix + opening quote) int arrowPosition = diffIndex - contextStart; string arrow = new string(' ', arrowPosition + 4); // +3 for " " prefix, +1 for opening quote message.AppendLine($"{arrow}↓"); +#if NET8_0_OR_GREATER + message.AppendLine($" \"{TruncateSpan(actualContext, 50)}\""); + message.AppendLine($" \"{TruncateSpan(expectedContext, 50)}\""); +#else message.AppendLine($" \"{TruncateString(actualContext, 50)}\""); message.AppendLine($" \"{TruncateString(expectedContext, 50)}\""); +#endif message.Append($"{arrow}↑"); return message.ToString(); @@ -202,9 +214,25 @@ private static string TruncateString(string? str, int maxLength) return str ?? ""; } +#if NET8_0_OR_GREATER + return string.Concat(str.AsSpan(0, maxLength), "…"); +#else return str.Substring(0, maxLength) + "…"; +#endif } +#if NET8_0_OR_GREATER + private static string TruncateSpan(ReadOnlySpan span, int maxLength) + { + if (span.Length <= maxLength) + { + return new string(span); + } + + return string.Concat(span.Slice(0, maxLength), "…"); + } +#endif + protected override string GetExpectation() { var comparisonDesc = _comparison == StringComparison.Ordinal @@ -213,4 +241,44 @@ protected override string GetExpectation() var truncatedExpected = TruncateString(_expected, 99); return $"to be equal to \"{truncatedExpected}\"{comparisonDesc}"; } + + private static string RemoveWhitespace(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + +#if NETSTANDARD2_0 + // Use LINQ for netstandard2.0 compatibility + return string.Concat(input.Where(c => !char.IsWhiteSpace(c))); +#elif NET8_0_OR_GREATER + // Use Span for better performance on modern .NET + Span buffer = input.Length <= 256 + ? stackalloc char[input.Length] + : new char[input.Length]; + + int writeIndex = 0; + foreach (char c in input) + { + if (!char.IsWhiteSpace(c)) + { + buffer[writeIndex++] = c; + } + } + + return new string(buffer.Slice(0, writeIndex)); +#else + // Use StringBuilder for other targets + var sb = new StringBuilder(input.Length); + foreach (char c in input) + { + if (!char.IsWhiteSpace(c)) + { + sb.Append(c); + } + } + return sb.ToString(); +#endif + } } diff --git a/TUnit.Core/AbstractExecutableTest.cs b/TUnit.Core/AbstractExecutableTest.cs index bf6252ac85..38ff5dc886 100644 --- a/TUnit.Core/AbstractExecutableTest.cs +++ b/TUnit.Core/AbstractExecutableTest.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using TUnit.Core.Helpers; using TUnit.Core.Models; namespace TUnit.Core; @@ -8,7 +9,7 @@ public abstract class AbstractExecutableTest { public required string TestId { get; init; } - public virtual TestMetadata Metadata { get; init; } = null!; + public TestMetadata Metadata { get; init; } = null!; public required object?[] Arguments { get; init; } @@ -70,15 +71,26 @@ public DateTimeOffset? StartTime public void SetResult(TestState state, Exception? exception = null) { State = state; + + // Lazy output building - avoid string allocation when there's no output + var output = Context.GetOutput(); + var errorOutput = Context.GetErrorOutput(); + var combinedOutput = string.Empty; + + if (output.Length > 0 || errorOutput.Length > 0) + { + combinedOutput = string.Concat(output, Environment.NewLine, Environment.NewLine, errorOutput); + } + Context.Execution.Result ??= new TestResult { State = state, Exception = exception, - ComputerName = Environment.MachineName, + ComputerName = EnvironmentHelper.MachineName, Duration = Duration, End = EndTime ??= DateTimeOffset.UtcNow, Start = StartTime ??= DateTimeOffset.UtcNow, - Output = Context.GetOutput() + Environment.NewLine + Environment.NewLine + Context.GetErrorOutput() + Output = combinedOutput }; } } diff --git a/TUnit.Core/Attributes/TestData/CombinedDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/CombinedDataSourcesAttribute.cs similarity index 92% rename from TUnit.Core/Attributes/TestData/CombinedDataSourceAttribute.cs rename to TUnit.Core/Attributes/TestData/CombinedDataSourcesAttribute.cs index 200d450a6e..e39d59f1bb 100644 --- a/TUnit.Core/Attributes/TestData/CombinedDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/CombinedDataSourcesAttribute.cs @@ -9,13 +9,13 @@ namespace TUnit.Core; /// /// /// -/// The allows you to apply different data source attributes +/// The allows you to apply different data source attributes /// (such as , , ) /// to individual parameters, creating test cases from all combinations via Cartesian product. /// /// /// This is different from which uses Matrix-specific attributes. -/// CombinedDataSource works with ANY , providing maximum flexibility +/// CombinedDataSources works with ANY , providing maximum flexibility /// for complex data-driven testing scenarios. /// /// @@ -36,7 +36,7 @@ namespace TUnit.Core; /// Basic Usage with Arguments: /// /// [Test] -/// [CombinedDataSource] +/// [CombinedDataSources] /// public void BasicTest( /// [Arguments(1, 2, 3)] int x, /// [Arguments("a", "b")] string y) @@ -55,7 +55,7 @@ namespace TUnit.Core; /// } /// /// [Test] -/// [CombinedDataSource] +/// [CombinedDataSources] /// public void MixedTest( /// [Arguments(1, 2)] int x, /// [MethodDataSource(nameof(GetStrings))] string y, @@ -69,7 +69,7 @@ namespace TUnit.Core; /// Multiple Data Sources on Same Parameter: /// /// [Test] -/// [CombinedDataSource] +/// [CombinedDataSources] /// public void MultipleSourcesTest( /// [Arguments(1, 2)] /// [Arguments(3, 4)] int x, @@ -81,7 +81,7 @@ namespace TUnit.Core; /// /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public sealed class CombinedDataSourceAttribute : AsyncUntypedDataSourceGeneratorAttribute, IAccessesInstanceData +public sealed class CombinedDataSourcesAttribute : AsyncUntypedDataSourceGeneratorAttribute, IAccessesInstanceData { protected override async IAsyncEnumerable>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata) { @@ -93,12 +93,12 @@ public sealed class CombinedDataSourceAttribute : AsyncUntypedDataSourceGenerato if (parameterInformation.Length != dataGeneratorMetadata.MembersToGenerate.Length || parameterInformation.Length is 0) { - throw new Exception("[CombinedDataSource] only supports parameterised tests"); + throw new Exception("[CombinedDataSources] only supports parameterised tests"); } if (dataGeneratorMetadata.TestInformation == null) { - throw new InvalidOperationException("CombinedDataSource requires test information but none is available. This may occur during static property initialization."); + throw new InvalidOperationException("CombinedDataSources requires test information but none is available. This may occur during static property initialization."); } // For each parameter, collect all possible values (individual values, not arrays) @@ -144,7 +144,7 @@ public sealed class CombinedDataSourceAttribute : AsyncUntypedDataSourceGenerato if (dataSourceAttributes.Length == 0) { - throw new InvalidOperationException($"Parameter '{parameterMetadata.Name}' has no data source attributes. All parameters must have at least one IDataSourceAttribute when using [CombinedDataSource]."); + throw new InvalidOperationException($"Parameter '{parameterMetadata.Name}' has no data source attributes. All parameters must have at least one IDataSourceAttribute when using [CombinedDataSources]."); } var allValues = new List(); @@ -191,8 +191,8 @@ public sealed class CombinedDataSourceAttribute : AsyncUntypedDataSourceGenerato { var values = new List(); - // Special handling for ArgumentsAttribute when used on parameters with CombinedDataSource - // ArgumentsAttribute yields ONE row containing ALL values, but for CombinedDataSource we need + // Special handling for ArgumentsAttribute when used on parameters with CombinedDataSources + // ArgumentsAttribute yields ONE row containing ALL values, but for CombinedDataSources we need // each value to be treated as a separate option for this parameter if (dataSourceAttr is ArgumentsAttribute argsAttr) { diff --git a/TUnit.Core/Helpers/ArgumentFormatter.cs b/TUnit.Core/Helpers/ArgumentFormatter.cs index a0d8ac4ba0..15c11f3333 100644 --- a/TUnit.Core/Helpers/ArgumentFormatter.cs +++ b/TUnit.Core/Helpers/ArgumentFormatter.cs @@ -91,7 +91,26 @@ private static string FormatDefault(object? o) { // Replace dots with middle dot (·) to prevent VS Test Explorer from interpreting them as namespace separators // Only do this if the string contains dots, to avoid unnecessary allocations - return str.Contains('.') ? str.Replace(".", "·") : str; + if (!str.Contains('.')) + { + return str; + } + +#if NET8_0_OR_GREATER + // Use Span for better performance - avoid string.Replace allocation + Span buffer = str.Length <= 256 + ? stackalloc char[str.Length] + : new char[str.Length]; + + for (int i = 0; i < str.Length; i++) + { + buffer[i] = str[i] == '.' ? '·' : str[i]; + } + + return new string(buffer); +#else + return str.Replace(".", "·"); +#endif } if (toString == type.FullName || toString == type.AssemblyQualifiedName) diff --git a/TUnit.Core/Helpers/EnvironmentHelper.cs b/TUnit.Core/Helpers/EnvironmentHelper.cs new file mode 100644 index 0000000000..294f2aca27 --- /dev/null +++ b/TUnit.Core/Helpers/EnvironmentHelper.cs @@ -0,0 +1,13 @@ +namespace TUnit.Core.Helpers; + +/// +/// Provides cached environment information to avoid repeated system calls. +/// +internal static class EnvironmentHelper +{ + /// + /// Cached machine name - avoids system call overhead on every test completion. + /// Environment.MachineName requires a P/Invoke call which is expensive when called millions of times. + /// + public static readonly string MachineName = Environment.MachineName; +} diff --git a/TUnit.Core/PropertyInjection/PropertySetterFactory.cs b/TUnit.Core/PropertyInjection/PropertySetterFactory.cs index 78ee5fc005..3d9dce651d 100644 --- a/TUnit.Core/PropertyInjection/PropertySetterFactory.cs +++ b/TUnit.Core/PropertyInjection/PropertySetterFactory.cs @@ -68,7 +68,15 @@ internal static class PropertySetterFactory } // Try underscore-prefixed camelCase name +#if NET8_0_OR_GREATER + Span buffer = stackalloc char[property.Name.Length + 1]; + buffer[0] = '_'; + buffer[1] = char.ToLowerInvariant(property.Name[0]); + property.Name.AsSpan(1).CopyTo(buffer.Slice(2)); + var underscoreName = new string(buffer); +#else var underscoreName = "_" + char.ToLowerInvariant(property.Name[0]) + property.Name.Substring(1); +#endif field = GetFieldSafe(declaringType, underscoreName, backingFieldFlags); if (field != null && field.FieldType == property.PropertyType) { diff --git a/TUnit.Core/PropertySourceRegistry.cs b/TUnit.Core/PropertySourceRegistry.cs index a023df4426..a95239b460 100644 --- a/TUnit.Core/PropertySourceRegistry.cs +++ b/TUnit.Core/PropertySourceRegistry.cs @@ -222,7 +222,15 @@ private static PropertyInjectionData CreatePropertyInjection(System.Reflection.P return field; } +#if NET8_0_OR_GREATER + Span buffer = stackalloc char[property.Name.Length + 1]; + buffer[0] = '_'; + buffer[1] = char.ToLowerInvariant(property.Name[0]); + property.Name.AsSpan(1).CopyTo(buffer.Slice(2)); + var underscoreName = new string(buffer); +#else var underscoreName = "_" + char.ToLowerInvariant(property.Name[0]) + property.Name.Substring(1); +#endif field = GetFieldSafe(declaringType, underscoreName, backingFieldFlags); if (field != null && field.FieldType == property.PropertyType) diff --git a/TUnit.Core/TestContext.Execution.cs b/TUnit.Core/TestContext.Execution.cs index 6f75db3580..1ac299776c 100644 --- a/TUnit.Core/TestContext.Execution.cs +++ b/TUnit.Core/TestContext.Execution.cs @@ -1,4 +1,5 @@ using TUnit.Core.Enums; +using TUnit.Core.Helpers; using TUnit.Core.Interfaces; namespace TUnit.Core; @@ -112,7 +113,7 @@ internal void OverrideResult(TestState state, string reason) End = DateTimeOffset.UtcNow, Duration = DateTimeOffset.UtcNow - (TestStart ?? DateTimeOffset.UtcNow), Exception = exceptionForResult, - ComputerName = Environment.MachineName, + ComputerName = EnvironmentHelper.MachineName, TestContext = this }; diff --git a/TUnit.Core/TestContext.Output.cs b/TUnit.Core/TestContext.Output.cs index e3ee62cddf..bbe9fad91f 100644 --- a/TUnit.Core/TestContext.Output.cs +++ b/TUnit.Core/TestContext.Output.cs @@ -12,7 +12,24 @@ public partial class TestContext // Internal backing fields and properties internal ConcurrentBag Timings { get; } = []; private readonly ConcurrentBag _artifactsBag = new(); - internal IReadOnlyList Artifacts => _artifactsBag.ToList(); + private List? _cachedArtifactsList; + private int _artifactsVersion = 0; + + internal IReadOnlyList Artifacts + { + get + { + // Simple version check - if bag hasn't changed, return cached list + if (_cachedArtifactsList != null && _artifactsVersion == _artifactsBag.Count) + { + return _cachedArtifactsList; + } + + _cachedArtifactsList = _artifactsBag.ToList(); + _artifactsVersion = _artifactsBag.Count; + return _cachedArtifactsList; + } + } // Explicit interface implementations for ITestOutput TextWriter ITestOutput.StandardOutput => OutputWriter; @@ -28,6 +45,7 @@ void ITestOutput.RecordTiming(Timing timing) void ITestOutput.AttachArtifact(Artifact artifact) { _artifactsBag.Add(artifact); + _artifactsVersion++; // Invalidate cache } string ITestOutput.GetStandardOutput() => GetOutput(); diff --git a/TUnit.Core/TestContext.Parallelization.cs b/TUnit.Core/TestContext.Parallelization.cs index 8bb935a56c..762b06acce 100644 --- a/TUnit.Core/TestContext.Parallelization.cs +++ b/TUnit.Core/TestContext.Parallelization.cs @@ -5,7 +5,7 @@ namespace TUnit.Core; public partial class TestContext { - internal IReadOnlyList ParallelConstraints => _parallelConstraints; + internal IReadOnlyList ParallelConstraints => _parallelConstraints ?? []; internal Priority ExecutionPriority { get; set; } = Priority.Normal; IReadOnlyList ITestParallelization.Constraints => ParallelConstraints; @@ -20,6 +20,7 @@ void ITestParallelization.AddConstraint(IParallelConstraint constraint) { if (constraint != null) { + _parallelConstraints ??= []; _parallelConstraints.Add(constraint); } } diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs index da8c809378..66d6da6b09 100644 --- a/TUnit.Core/TestContext.cs +++ b/TUnit.Core/TestContext.cs @@ -103,7 +103,7 @@ public static string WorkingDirectory internal Type? DisplayNameFormatter { get; set; } // New: Support multiple parallel constraints - private readonly List _parallelConstraints = []; + private List? _parallelConstraints; /// @@ -111,9 +111,9 @@ public static string WorkingDirectory /// public ClassHookContext ClassContext { get; } - internal List> ArgumentDisplayFormatters { get; } = - [ - ]; + private List>? _argumentDisplayFormatters; + internal List> ArgumentDisplayFormatters => + _argumentDisplayFormatters ??= []; internal DiscoveredTest? InternalDiscoveredTest { get; set; } @@ -149,7 +149,9 @@ internal override void SetAsyncLocalContext() internal AbstractExecutableTest InternalExecutableTest { get; set; } = null!; - internal ConcurrentDictionary> TrackedObjects { get; } = []; + private ConcurrentDictionary>? _trackedObjects; + internal ConcurrentDictionary> TrackedObjects => + _trackedObjects ??= new(); } diff --git a/TUnit.Engine/Building/TestBuilderPipeline.cs b/TUnit.Engine/Building/TestBuilderPipeline.cs index 6c2312c760..78aef3a896 100644 --- a/TUnit.Engine/Building/TestBuilderPipeline.cs +++ b/TUnit.Engine/Building/TestBuilderPipeline.cs @@ -390,7 +390,7 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada End = DateTimeOffset.UtcNow, Duration = TimeSpan.Zero, Exception = exception, - ComputerName = Environment.MachineName, + ComputerName = EnvironmentHelper.MachineName, TestContext = context } }; @@ -441,7 +441,7 @@ private AbstractExecutableTest CreateFailedTestForGenericResolutionError(TestMet End = DateTimeOffset.UtcNow, Duration = TimeSpan.Zero, Exception = exception, - ComputerName = Environment.MachineName, + ComputerName = EnvironmentHelper.MachineName, TestContext = context } }; diff --git a/TUnit.Engine/Extensions/TestContextExtensions.cs b/TUnit.Engine/Extensions/TestContextExtensions.cs index aceb5accd7..e9637f4259 100644 --- a/TUnit.Engine/Extensions/TestContextExtensions.cs +++ b/TUnit.Engine/Extensions/TestContextExtensions.cs @@ -4,16 +4,32 @@ namespace TUnit.Engine.Extensions; internal static class TestContextExtensions { - private static IEnumerable GetInternal(TestContext testContext) => - [ - testContext.ClassConstructor, - testContext.Events, - ..testContext.Metadata.TestDetails.TestClassArguments, - testContext.Metadata.TestDetails.ClassInstance, - ..testContext.Metadata.TestDetails.GetAllAttributes(), - ..testContext.Metadata.TestDetails.TestMethodArguments, - ..testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments.Select(x => x.Value), - ]; + private static object?[] GetInternal(TestContext testContext) + { + var testClassArgs = testContext.Metadata.TestDetails.TestClassArguments; + var attributes = testContext.Metadata.TestDetails.GetAllAttributes(); + var testMethodArgs = testContext.Metadata.TestDetails.TestMethodArguments; + var injectedProps = testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments; + + // Pre-calculate capacity to avoid reallocations + var capacity = 3 + testClassArgs.Length + attributes.Count + testMethodArgs.Length + injectedProps.Count; + var result = new List(capacity); + + result.Add(testContext.ClassConstructor); + result.Add(testContext.Events); + result.AddRange(testClassArgs); + result.Add(testContext.Metadata.TestDetails.ClassInstance); + result.AddRange(attributes); + result.AddRange(testMethodArgs); + + // Manual loop instead of .Select() to avoid LINQ allocation + foreach (var prop in injectedProps) + { + result.Add(prop.Value); + } + + return result.ToArray(); + } public static IEnumerable GetEligibleEventObjects(this TestContext testContext) { diff --git a/TUnit.Engine/Services/EventReceiverOrchestrator.cs b/TUnit.Engine/Services/EventReceiverOrchestrator.cs index c09da0b98a..ed44dc8666 100644 --- a/TUnit.Engine/Services/EventReceiverOrchestrator.cs +++ b/TUnit.Engine/Services/EventReceiverOrchestrator.cs @@ -93,10 +93,28 @@ public async ValueTask InvokeTestStartEventReceiversAsync(TestContext context, C private async ValueTask InvokeTestStartEventReceiversCore(TestContext context, CancellationToken cancellationToken) { - var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes( - context.GetEligibleEventObjects() - .OfType() - .OrderBy(static r => r.Order)); + // Manual filtering and sorting instead of LINQ to avoid allocations + var eligibleObjects = context.GetEligibleEventObjects(); + List? receivers = null; + + foreach (var obj in eligibleObjects) + { + if (obj is ITestStartEventReceiver receiver) + { + receivers ??= []; + receivers.Add(receiver); + } + } + + if (receivers == null) + { + return; + } + + // Manual sort instead of OrderBy + receivers.Sort((a, b) => a.Order.CompareTo(b.Order)); + + var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes(receivers); foreach (var receiver in filteredReceivers) { @@ -117,11 +135,28 @@ public async ValueTask InvokeTestEndEventReceiversAsync(TestContext context, Can private async ValueTask InvokeTestEndEventReceiversCore(TestContext context, CancellationToken cancellationToken) { - // Filter scoped attributes - FilterScopedAttributes will materialize the collection - var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes( - context.GetEligibleEventObjects() - .OfType() - .OrderBy(static r => r.Order)); + // Manual filtering and sorting instead of LINQ to avoid allocations + var eligibleObjects = context.GetEligibleEventObjects(); + List? receivers = null; + + foreach (var obj in eligibleObjects) + { + if (obj is ITestEndEventReceiver receiver) + { + receivers ??= []; + receivers.Add(receiver); + } + } + + if (receivers == null) + { + return; + } + + // Manual sort instead of OrderBy + receivers.Sort((a, b) => a.Order.CompareTo(b.Order)); + + var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes(receivers); foreach (var receiver in filteredReceivers) { @@ -149,11 +184,28 @@ public async ValueTask InvokeTestSkippedEventReceiversAsync(TestContext context, private async ValueTask InvokeTestSkippedEventReceiversCore(TestContext context, CancellationToken cancellationToken) { - // Filter scoped attributes - FilterScopedAttributes will materialize the collection - var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes( - context.GetEligibleEventObjects() - .OfType() - .OrderBy(static r => r.Order)); + // Manual filtering and sorting instead of LINQ to avoid allocations + var eligibleObjects = context.GetEligibleEventObjects(); + List? receivers = null; + + foreach (var obj in eligibleObjects) + { + if (obj is ITestSkippedEventReceiver receiver) + { + receivers ??= []; + receivers.Add(receiver); + } + } + + if (receivers == null) + { + return; + } + + // Manual sort instead of OrderBy + receivers.Sort((a, b) => a.Order.CompareTo(b.Order)); + + var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes(receivers); foreach (var receiver in filteredReceivers) { diff --git a/TUnit.Engine/Services/TestDependencyResolver.cs b/TUnit.Engine/Services/TestDependencyResolver.cs index 2c4c83bbf5..0a9191752b 100644 --- a/TUnit.Engine/Services/TestDependencyResolver.cs +++ b/TUnit.Engine/Services/TestDependencyResolver.cs @@ -1,4 +1,5 @@ using TUnit.Core; +using TUnit.Core.Helpers; namespace TUnit.Engine.Services; @@ -263,7 +264,7 @@ private static void CreateDependencyResolutionFailedResult(AbstractExecutableTes Duration = TimeSpan.Zero, Exception = new InvalidOperationException( $"Could not resolve all dependencies for test {test.Metadata.TestClassType.Name}.{test.Metadata.TestMethodName}"), - ComputerName = Environment.MachineName + ComputerName = EnvironmentHelper.MachineName }; } } diff --git a/TUnit.Engine/Services/TestExecution/TestStateManager.cs b/TUnit.Engine/Services/TestExecution/TestStateManager.cs index bfb0ef6486..dee589a881 100644 --- a/TUnit.Engine/Services/TestExecution/TestStateManager.cs +++ b/TUnit.Engine/Services/TestExecution/TestStateManager.cs @@ -1,5 +1,6 @@ using TUnit.Core; using TUnit.Core.Exceptions; +using TUnit.Core.Helpers; namespace TUnit.Engine.Services.TestExecution; @@ -27,7 +28,7 @@ public Task MarkCompletedAsync(AbstractExecutableTest test) End = now, Duration = now - test.StartTime.GetValueOrDefault(), Exception = null, - ComputerName = Environment.MachineName + ComputerName = EnvironmentHelper.MachineName }; test.State = test.Result.State; @@ -55,7 +56,7 @@ public Task MarkFailedAsync(AbstractExecutableTest test, Exception exception) Start = test.StartTime, End = test.EndTime, Duration = test.EndTime - test.StartTime.GetValueOrDefault(), - ComputerName = Environment.MachineName + ComputerName = EnvironmentHelper.MachineName }; } @@ -81,7 +82,7 @@ public Task MarkSkippedAsync(AbstractExecutableTest test, string reason) Start = test.StartTime.Value, End = test.EndTime, Duration = test.EndTime - test.StartTime.GetValueOrDefault(), - ComputerName = Environment.MachineName + ComputerName = EnvironmentHelper.MachineName }; return Task.CompletedTask; @@ -98,7 +99,7 @@ public Task MarkCircularDependencyFailedAsync(AbstractExecutableTest test, Excep Start = now, End = now, Duration = TimeSpan.Zero, - ComputerName = Environment.MachineName + ComputerName = EnvironmentHelper.MachineName }; return Task.CompletedTask; @@ -117,7 +118,7 @@ public Task MarkDependencyResolutionFailedAsync(AbstractExecutableTest test, Exc Start = now, End = now, Duration = TimeSpan.Zero, - ComputerName = Environment.MachineName + ComputerName = EnvironmentHelper.MachineName }; return Task.CompletedTask; diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index ab5ced53db..d14bf94345 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -26,7 +26,7 @@ namespace public ? EndTime { get; set; } public .? ExecutionContext { get; set; } public .? ExecutionTask { get; } - public virtual .TestMetadata Metadata { get; init; } + public .TestMetadata Metadata { get; init; } public .TestResult? Result { get; set; } public ? StartTime { get; set; } public .TestState State { get; set; } @@ -371,10 +371,10 @@ namespace public static .ClassMetadata GetOrAdd(string name, <.ClassMetadata> factory) { } } [(.Class | .Method)] - public sealed class CombinedDataSourceAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData + public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData { - public CombinedDataSourceAttribute() { } - [.(typeof(.CombinedDataSourceAttribute.d__0))] + public CombinedDataSourcesAttribute() { } + [.(typeof(.CombinedDataSourcesAttribute.d__0))] protected override .<<.>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } public sealed class ConcreteType : .TypeInfo, <.ConcreteType> diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index d36d30038c..4e5bebf168 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -26,7 +26,7 @@ namespace public ? EndTime { get; set; } public .? ExecutionContext { get; set; } public .? ExecutionTask { get; } - public virtual .TestMetadata Metadata { get; init; } + public .TestMetadata Metadata { get; init; } public .TestResult? Result { get; set; } public ? StartTime { get; set; } public .TestState State { get; set; } @@ -371,10 +371,10 @@ namespace public static .ClassMetadata GetOrAdd(string name, <.ClassMetadata> factory) { } } [(.Class | .Method)] - public sealed class CombinedDataSourceAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData + public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData { - public CombinedDataSourceAttribute() { } - [.(typeof(.CombinedDataSourceAttribute.d__0))] + public CombinedDataSourcesAttribute() { } + [.(typeof(.CombinedDataSourcesAttribute.d__0))] protected override .<<.>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } public sealed class ConcreteType : .TypeInfo, <.ConcreteType> diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index ec78f03a52..0d1892cdaa 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -26,7 +26,7 @@ namespace public ? EndTime { get; set; } public .? ExecutionContext { get; set; } public .? ExecutionTask { get; } - public virtual .TestMetadata Metadata { get; init; } + public .TestMetadata Metadata { get; init; } public .TestResult? Result { get; set; } public ? StartTime { get; set; } public .TestState State { get; set; } @@ -371,10 +371,10 @@ namespace public static .ClassMetadata GetOrAdd(string name, <.ClassMetadata> factory) { } } [(.Class | .Method)] - public sealed class CombinedDataSourceAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData + public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData { - public CombinedDataSourceAttribute() { } - [.(typeof(.CombinedDataSourceAttribute.d__0))] + public CombinedDataSourcesAttribute() { } + [.(typeof(.CombinedDataSourcesAttribute.d__0))] protected override .<<.>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } public sealed class ConcreteType : .TypeInfo, <.ConcreteType> diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt index a138c6c86b..f86e47e167 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -26,7 +26,7 @@ namespace public ? EndTime { get; set; } public .? ExecutionContext { get; set; } public .? ExecutionTask { get; } - public virtual .TestMetadata Metadata { get; init; } + public .TestMetadata Metadata { get; init; } public .TestResult? Result { get; set; } public ? StartTime { get; set; } public .TestState State { get; set; } @@ -351,10 +351,10 @@ namespace public static .ClassMetadata GetOrAdd(string name, <.ClassMetadata> factory) { } } [(.Class | .Method)] - public sealed class CombinedDataSourceAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData + public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData { - public CombinedDataSourceAttribute() { } - [.(typeof(.CombinedDataSourceAttribute.d__0))] + public CombinedDataSourcesAttribute() { } + [.(typeof(.CombinedDataSourcesAttribute.d__0))] protected override .<<.>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } public sealed class ConcreteType : .TypeInfo, <.ConcreteType> diff --git a/TUnit.TestProject/CombinedDataSourceErrorTests.cs b/TUnit.TestProject/CombinedDataSourceErrorTests.cs index f09defcb95..e88e4ebd4c 100644 --- a/TUnit.TestProject/CombinedDataSourceErrorTests.cs +++ b/TUnit.TestProject/CombinedDataSourceErrorTests.cs @@ -3,7 +3,7 @@ namespace TUnit.TestProject; /// -/// Tests for error scenarios with CombinedDataSource +/// Tests for error scenarios with CombinedDataSources /// These tests are expected to fail during test initialization /// public class CombinedDataSourceErrorTests @@ -12,7 +12,7 @@ public class CombinedDataSourceErrorTests // They should fail during test discovery/initialization, not during execution // [Test] - // [CombinedDataSource] + // [CombinedDataSources] // public async Task ParameterWithoutDataSource_ShouldFail( // [Arguments(1, 2)] int x, // int y) // Missing data source attribute - should fail @@ -21,7 +21,7 @@ public class CombinedDataSourceErrorTests // } // [Test] - // [CombinedDataSource] + // [CombinedDataSources] // public async Task NoParametersWithDataSources_ShouldFail() // { // // Should fail because there are no parameters with data sources @@ -44,7 +44,7 @@ public static IEnumerable GetEmptyStrings() // Note: SkipIfEmpty is not a property of MethodDataSource // This test is commented out for now // [Test] - // [CombinedDataSource] + // [CombinedDataSources] // public async Task EmptyDataSource_ShouldHandleGracefully( // [Arguments(1)] int x, // [MethodDataSource(nameof(GetEmptyStrings))] string y) @@ -54,7 +54,7 @@ public static IEnumerable GetEmptyStrings() // } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task ParameterWithNullValues( [Arguments(null, 1, 2)] int? x, [Arguments(null, "a")] string? y) diff --git a/TUnit.TestProject/CombinedDataSourceTests.cs b/TUnit.TestProject/CombinedDataSourceTests.cs index e853a9eb1c..bb0af14e4b 100644 --- a/TUnit.TestProject/CombinedDataSourceTests.cs +++ b/TUnit.TestProject/CombinedDataSourceTests.cs @@ -28,7 +28,7 @@ public static IEnumerable GetBools() #region Basic Tests - Arguments Only [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task TwoParameters_Arguments( [Arguments(1, 2, 3)] int x, [Arguments("a", "b")] string y) @@ -39,7 +39,7 @@ public async Task TwoParameters_Arguments( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task ThreeParameters_Arguments( [Arguments(1, 2)] int x, [Arguments("a", "b", "c")] string y, @@ -52,7 +52,7 @@ public async Task ThreeParameters_Arguments( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task FourParameters_Arguments( [Arguments(1, 2)] int w, [Arguments("a", "b")] string x, @@ -71,7 +71,7 @@ public async Task FourParameters_Arguments( #region Mixing Arguments with MethodDataSource [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task ArgumentsWithMethodDataSource( [Arguments(1, 2)] int x, [MethodDataSource(nameof(GetStrings))] string y) @@ -82,7 +82,7 @@ public async Task ArgumentsWithMethodDataSource( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task MultipleMethodDataSources( [MethodDataSource(nameof(GetNumbers))] int x, [MethodDataSource(nameof(GetStrings))] string y) @@ -93,7 +93,7 @@ public async Task MultipleMethodDataSources( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task ThreeWayMix_ArgumentsAndMethodDataSources( [Arguments(1, 2)] int x, [MethodDataSource(nameof(GetStrings))] string y, @@ -110,7 +110,7 @@ public async Task ThreeWayMix_ArgumentsAndMethodDataSources( #region Multiple Attributes on Same Parameter [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task MultipleArgumentsAttributesOnSameParameter( [Arguments(1, 2)] [Arguments(3, 4)] int x, @@ -122,7 +122,7 @@ public async Task MultipleArgumentsAttributesOnSameParameter( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task MixingMultipleDataSourcesPerParameter( [Arguments(1)] [MethodDataSource(nameof(GetNumbers))] int x, @@ -138,7 +138,7 @@ public async Task MixingMultipleDataSourcesPerParameter( #region Type Variety Tests [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task DifferentPrimitiveTypes( [Arguments(1, 2)] int intVal, [Arguments("a", "b")] string stringVal, @@ -155,7 +155,7 @@ public async Task DifferentPrimitiveTypes( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task NullableTypes( [Arguments(1, 2, null)] int? nullableInt, [Arguments("a", null)] string? nullableString) @@ -173,7 +173,7 @@ public async Task NullableTypes( #region Edge Cases [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task SingleParameterWithSingleValue( [Arguments(42)] int x) { @@ -182,7 +182,7 @@ public async Task SingleParameterWithSingleValue( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task SingleParameterWithMultipleValues( [Arguments(1, 2, 3, 4, 5)] int x) { @@ -191,7 +191,7 @@ public async Task SingleParameterWithMultipleValues( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task ManyParametersSmallSets( [Arguments(1)] int a, [Arguments(2)] int b, @@ -218,7 +218,7 @@ public class SimpleClass } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task WithClassDataSource( [Arguments(1, 2)] int x, [ClassDataSource] SimpleClass obj) @@ -241,7 +241,7 @@ public static IEnumerable GetGenericValues(T first, T second) // Note: MethodDataSource with generic parameters and arguments needs special syntax // This test is simplified for now [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task WithTypedMethodDataSource( [Arguments(1, 2)] int x, [MethodDataSource(nameof(GetNumbers))] int y) @@ -259,7 +259,7 @@ public async Task WithTypedMethodDataSource( private static readonly object _lock = new(); [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task VerifyCartesianProduct_TwoParameters( [Arguments("A", "B")] string x, [Arguments(1, 2, 3)] int y) @@ -283,7 +283,7 @@ public async Task VerifyCartesianProduct_TwoParameters( #region Performance Test - Many Combinations [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task LargeCartesianProduct( [Arguments(1, 2, 3, 4, 5)] int a, [Arguments(1, 2, 3, 4)] int b, @@ -325,7 +325,7 @@ public class Address } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task CombinedDataSource_WithPropertyInjection_SingleLevel( [Arguments(1, 2)] int x, [ClassDataSource] PersonWithPropertyInjection person) @@ -379,7 +379,7 @@ public class Country } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task CombinedDataSource_WithNestedPropertyInjection( [Arguments("A", "B")] string x, [ClassDataSource] PersonWithNestedPropertyInjection person) @@ -399,7 +399,7 @@ public async Task CombinedDataSource_WithNestedPropertyInjection( } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task CombinedDataSource_MultipleParametersWithPropertyInjection( [Arguments(1, 2)] int x, [ClassDataSource] PersonWithPropertyInjection person1, @@ -441,7 +441,7 @@ public async Task InitializeAsync() } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task CombinedDataSource_WithIAsyncInitializer( [Arguments(1, 2, 3)] int x, [ClassDataSource] InitializableClass obj) @@ -470,7 +470,7 @@ public async Task InitializeAsync() } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task CombinedDataSource_MultipleParametersWithIAsyncInitializer( [Arguments("Query1", "Query2")] string query, [ClassDataSource] DatabaseConnection db) @@ -522,7 +522,7 @@ public class Configuration } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task CombinedDataSource_WithPropertyInjectionAndIAsyncInitializer( [Arguments(10, 20)] int x, [ClassDataSource] InitializablePersonWithPropertyInjection person) @@ -594,7 +594,7 @@ public class Coordinates } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task CombinedDataSource_WithNestedPropertyInjectionAndMultipleIAsyncInitializers( [Arguments(true, false)] bool flag, [ClassDataSource] InitializableAddressWithNestedInjection address) @@ -623,7 +623,7 @@ public async Task CombinedDataSource_WithNestedPropertyInjectionAndMultipleIAsyn } [Test] - [CombinedDataSource] + [CombinedDataSources] public async Task CombinedDataSource_ComplexScenario_MultipleParametersWithMixedFeatures( [Arguments(1, 2)] int x, [MethodDataSource(nameof(GetStrings))] string y, @@ -632,7 +632,7 @@ public async Task CombinedDataSource_ComplexScenario_MultipleParametersWithMixed [ClassDataSource] InitializablePersonWithPropertyInjection personWithBoth) { // Should create 2 × 2 × 1 × 1 × 1 = 4 test cases - // This tests that CombinedDataSource handles: + // This tests that CombinedDataSources handles: // - Primitive arguments // - Method data sources // - IAsyncInitializer objects diff --git a/docs/docs/test-authoring/combined-data-source-summary.md b/docs/docs/test-authoring/combined-data-source-summary.md index d0bc3d4b24..fec77d7ef4 100644 --- a/docs/docs/test-authoring/combined-data-source-summary.md +++ b/docs/docs/test-authoring/combined-data-source-summary.md @@ -1,14 +1,14 @@ -# CombinedDataSource - Quick Reference +# CombinedDataSources - Quick Reference ## What is it? -`[CombinedDataSource]` allows you to apply different data source attributes to individual test method parameters, automatically generating all possible combinations (Cartesian product). +`[CombinedDataSources]` allows you to apply different data source attributes to individual test method parameters, automatically generating all possible combinations (Cartesian product). ## Quick Start ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task MyTest( [Arguments(1, 2, 3)] int x, [MethodDataSource(nameof(GetStrings))] string y) @@ -56,7 +56,7 @@ With 3 parameters: ### Pattern 1: All Arguments ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public void Test( [Arguments(1, 2)] int a, [Arguments("x", "y")] string b) @@ -68,7 +68,7 @@ public void Test( ### Pattern 2: Mixed Sources ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public void Test( [Arguments(1, 2)] int a, [MethodDataSource(nameof(GetData))] string b, @@ -81,7 +81,7 @@ public void Test( ### Pattern 3: Multiple Per Parameter ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public void Test( [Arguments(1, 2)] [Arguments(3, 4)] int a, // Combines to 4 values @@ -93,7 +93,7 @@ public void Test( ## When to Use -✅ **Use CombinedDataSource when:** +✅ **Use CombinedDataSources when:** - Different parameters need different data sources - You want maximum flexibility in data generation - You need to test all combinations of inputs @@ -118,7 +118,7 @@ public void Test( ## Full Documentation -See [CombinedDataSource](combined-data-source.md) for complete documentation including: +See [CombinedDataSources](combined-data-source.md) for complete documentation including: - Advanced scenarios - Error handling - AOT compilation details diff --git a/docs/docs/test-authoring/combined-data-source.md b/docs/docs/test-authoring/combined-data-source.md index 663a77143c..40fa2985ff 100644 --- a/docs/docs/test-authoring/combined-data-source.md +++ b/docs/docs/test-authoring/combined-data-source.md @@ -1,8 +1,8 @@ -# CombinedDataSource +# CombinedDataSources ## Overview -The `[CombinedDataSource]` attribute enables you to apply different data source attributes to individual parameters, creating test cases through Cartesian product combination. This provides maximum flexibility when you need different parameters to be generated by different data sources. +The `[CombinedDataSources]` attribute enables you to apply different data source attributes to individual parameters, creating test cases through Cartesian product combination. This provides maximum flexibility when you need different parameters to be generated by different data sources. ## Key Features @@ -14,7 +14,7 @@ The `[CombinedDataSource]` attribute enables you to apply different data source ## Comparison with MatrixDataSource -| Feature | MatrixDataSource | CombinedDataSource | +| Feature | MatrixDataSource | CombinedDataSources | |---------|-----------------|---------------------------| | Parameter-level attributes | `[Matrix]` only | ANY `IDataSourceAttribute` | | Combination strategy | Cartesian product | Cartesian product | @@ -27,7 +27,7 @@ The `[CombinedDataSource]` attribute enables you to apply different data source ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task SimpleTest( [Arguments(1, 2, 3)] int x, [Arguments("a", "b")] string y) @@ -50,7 +50,7 @@ public static IEnumerable GetStrings() } [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task MixedDataSources( [Arguments(1, 2)] int x, [MethodDataSource(nameof(GetStrings))] string y) @@ -76,7 +76,7 @@ public static IEnumerable GetNumbers() } [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task ThreeWayMix( [Arguments(1, 2)] int x, [MethodDataSource(nameof(GetNumbers))] int y, @@ -95,7 +95,7 @@ You can apply multiple data source attributes to a single parameter - all values ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task MultipleSourcesPerParameter( [Arguments(1, 2)] [Arguments(3, 4)] int x, @@ -118,7 +118,7 @@ public class MyTestData } [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task WithClassDataSource( [Arguments(1, 2)] int x, [ClassDataSource] MyTestData obj) @@ -133,7 +133,7 @@ public async Task WithClassDataSource( ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task DifferentTypes( [Arguments(1, 2)] int intVal, [Arguments("a", "b")] string stringVal, @@ -148,7 +148,7 @@ public async Task DifferentTypes( ## Cartesian Product Behavior -The `[CombinedDataSource]` generates test cases using **Cartesian product** - every combination of parameter values is tested. +The `[CombinedDataSources]` generates test cases using **Cartesian product** - every combination of parameter values is tested. ### Example Calculation @@ -168,7 +168,7 @@ Generated combinations: ## Supported Data Source Attributes -The following attributes can be applied to parameters with `[CombinedDataSource]`: +The following attributes can be applied to parameters with `[CombinedDataSources]`: ### Built-in Attributes @@ -196,7 +196,7 @@ public class CustomDataSourceAttribute : DataSourceGeneratorAttribute } [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task WithCustomDataSource( [Arguments(1, 2)] int x, [CustomDataSource] string y) @@ -251,7 +251,7 @@ Be aware of exponential growth with multiple parameters: ```csharp // ❌ ERROR: Parameter 'y' has no data source [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task MissingDataSource( [Arguments(1, 2)] int x, int y) // No data source attribute! @@ -260,21 +260,21 @@ public async Task MissingDataSource( } ``` -**Error**: `Parameter 'y' has no data source attributes. All parameters must have at least one IDataSourceAttribute when using [CombinedDataSource].` +**Error**: `Parameter 'y' has no data source attributes. All parameters must have at least one IDataSourceAttribute when using [CombinedDataSources].` ### No Parameters ```csharp // ❌ ERROR: No parameters with data sources [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task NoParameters() { // This will fail } ``` -**Error**: `[CombinedDataSource] only supports parameterised tests` +**Error**: `[CombinedDataSources] only supports parameterised tests` ### Nullable Types @@ -282,7 +282,7 @@ Nullable types are fully supported: ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task NullableTypes( [Arguments(1, 2, null)] int? nullableInt, [Arguments("a", null)] string? nullableString) @@ -314,7 +314,7 @@ public async Task OldWay(int x, string y) **MixedParameters:** ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task NewWay( [Arguments(1, 2)] int x, [Arguments("a", "b")] string y) @@ -340,7 +340,7 @@ public async Task MatrixWay( **MixedParameters:** ```csharp [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task MixedWay( [Arguments(1, 2)] int x, [MethodDataSource(nameof(GetStrings))] string y) @@ -351,7 +351,7 @@ public async Task MixedWay( ## AOT/Native Compilation -`[CombinedDataSource]` is fully compatible with AOT and Native compilation. The attribute uses proper trimming annotations and works in both source-generated and reflection modes. +`[CombinedDataSources]` is fully compatible with AOT and Native compilation. The attribute uses proper trimming annotations and works in both source-generated and reflection modes. ## Examples from Real-World Scenarios @@ -366,7 +366,7 @@ public static IEnumerable GetHttpMethods() } [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task ApiEndpoint_ResponseCodes( [MethodDataSource(nameof(GetHttpMethods))] HttpMethod method, [Arguments("/api/users", "/api/products")] string endpoint, @@ -387,7 +387,7 @@ public class QueryParameters } [Test] -[CombinedDataSource] +[CombinedDataSources] public async Task Database_Pagination( [Arguments(10, 20, 50)] int pageSize, [Arguments("asc", "desc")] string sortOrder,