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
18 changes: 9 additions & 9 deletions TUnit.Analyzers.Tests/CombinedDataSourceAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ await Verifier

public class MyClass
{
[CombinedDataSource]
[CombinedDataSources]
[Test]
public void MyTest(
[Arguments(1, 2, 3)] int value,
Expand Down Expand Up @@ -61,7 +61,7 @@ await Verifier

public class MyClass
{
[CombinedDataSource]
[CombinedDataSources]
[Test]
public void MyTest(
[Arguments(1, 2, 3)] int value,
Expand All @@ -87,7 +87,7 @@ await Verifier

public class MyClass
{
[CombinedDataSource]
[CombinedDataSources]
[MatrixDataSource]
[Test]
public void {|#0:MyTest|}(
Expand All @@ -111,7 +111,7 @@ await Verifier
"""
using TUnit.Core;

[CombinedDataSource]
[CombinedDataSources]
public class MyClass
{
public MyClass(
Expand Down Expand Up @@ -169,7 +169,7 @@ await Verifier

public class MyClass
{
[CombinedDataSource]
[CombinedDataSources]
[Test]
public void MyTest(
[Arguments(1, 2, 3)] int value,
Expand All @@ -195,7 +195,7 @@ public class MyClass
{
public static IEnumerable<int> GetNumbers() => [1, 2, 3];

[CombinedDataSource]
[CombinedDataSources]
[Test]
public void MyTest(
[MethodDataSource(nameof(GetNumbers))] int number,
Expand Down Expand Up @@ -229,7 +229,7 @@ protected override IEnumerable<Func<int>> GenerateDataSources(DataGeneratorMetad

public class MyClass
{
[CombinedDataSource]
[CombinedDataSources]
[Test]
public void MyTest(
[ClassDataSource<TestData>] int number,
Expand All @@ -252,7 +252,7 @@ await Verifier

public class MyClass
{
[CombinedDataSource]
[CombinedDataSources]
[Test]
public void MyTest(
[Arguments(1, 2, 3)] int value,
Expand Down Expand Up @@ -300,7 +300,7 @@ await Verifier
"""
using TUnit.Core;

[CombinedDataSource]
[CombinedDataSources]
public class MyClass
{
public MyClass(
Expand Down
8 changes: 4 additions & 4 deletions TUnit.Analyzers/CombinedDataSourceAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace TUnit.Analyzers;

/// <summary>
/// Analyzer for CombinedDataSource validation
/// Analyzer for CombinedDataSources validation
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CombinedDataSourceAnalyzer : ConcurrentDiagnosticAnalyzer
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Analyzers/Helpers/WellKnown.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
14 changes: 7 additions & 7 deletions TUnit.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -463,28 +463,28 @@
<value>Data source may produce no tests</value>
</data>
<data name="TUnit0070Description" xml:space="preserve">
<value>When parameters have data source attributes, the method or class must be marked with [CombinedDataSource] to combine the parameter data sources.</value>
<value>When parameters have data source attributes, the method or class must be marked with [CombinedDataSources] to combine the parameter data sources.</value>
</data>
<data name="TUnit0070MessageFormat" xml:space="preserve">
<value>[CombinedDataSource] is required when parameters have data source attributes.</value>
<value>[CombinedDataSources] is required when parameters have data source attributes.</value>
</data>
<data name="TUnit0070Title" xml:space="preserve">
<value>[CombinedDataSource] attribute required</value>
<value>[CombinedDataSources] attribute required</value>
</data>
<data name="TUnit0071Description" xml:space="preserve">
<value>When [CombinedDataSource] is used, all parameters (except CancellationToken) must have data source attributes to provide test data.</value>
<value>When [CombinedDataSources] is used, all parameters (except CancellationToken) must have data source attributes to provide test data.</value>
</data>
<data name="TUnit0071MessageFormat" xml:space="preserve">
<value>Parameter '{0}' is missing a data source attribute. All parameters must have data sources when using [CombinedDataSource].</value>
<value>Parameter '{0}' is missing a data source attribute. All parameters must have data sources when using [CombinedDataSources].</value>
</data>
<data name="TUnit0071Title" xml:space="preserve">
<value>Parameter missing data source attribute</value>
</data>
<data name="TUnit0072Description" xml:space="preserve">
<value>Using [CombinedDataSource] together with [MatrixDataSource] is not recommended as they serve different purposes and may cause confusion.</value>
<value>Using [CombinedDataSources] together with [MatrixDataSource] is not recommended as they serve different purposes and may cause confusion.</value>
</data>
<data name="TUnit0072MessageFormat" xml:space="preserve">
<value>[CombinedDataSource] should not be used with [MatrixDataSource]. Use one or the other.</value>
<value>[CombinedDataSources] should not be used with [MatrixDataSource]. Use one or the other.</value>
</data>
<data name="TUnit0072Title" xml:space="preserve">
<value>Conflicting data source attributes</value>
Expand Down
13 changes: 12 additions & 1 deletion TUnit.Assertions/AssertionScope.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.ExceptionServices;
using System.Text;
using TUnit.Assertions.Exceptions;

namespace TUnit.Assertions;
Expand Down Expand Up @@ -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));
}

Expand Down
21 changes: 20 additions & 1 deletion TUnit.Assertions/Conditions/StringAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,34 @@ 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))
{
sb.Append(c);
}
}
return sb.ToString();
#else
// Use Span<char> for better performance on modern .NET
Span<char> 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}\"";
Expand Down
72 changes: 70 additions & 2 deletions TUnit.Assertions/Conditions/StringEqualsAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<string> 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)
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<char> 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
Expand All @@ -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<char> for better performance on modern .NET
Span<char> 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
}
}
18 changes: 15 additions & 3 deletions TUnit.Core/AbstractExecutableTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using TUnit.Core.Helpers;
using TUnit.Core.Models;

namespace TUnit.Core;
Expand All @@ -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; }

Expand Down Expand Up @@ -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
};
}
}
Loading
Loading