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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Core.SourceGenerator.Extensions;
Expand Down Expand Up @@ -386,10 +387,46 @@ private bool AreValuesEqual(object? enumValue, object? providedValue)

private static string EscapeForTestId(string str)
{
return str.Replace("\\", "\\\\")
.Replace("\r", "\\r")
.Replace("\n", "\\n")
.Replace("\t", "\\t")
.Replace("\"", "\\\"");
var needsEscape = false;
foreach (var c in str)
{
if (c == '\\' || c == '\r' || c == '\n' || c == '\t' || c == '"')
{
needsEscape = true;
break;
}
}
Comment on lines +391 to +398
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since .NET 8 it is possible to do this: var needsEscape = str.AsSpan().ContainsAny(['\\', '\r', '\n', '\t', '"']);


if (!needsEscape)
{
return str;
}

var builder = new StringBuilder(str.Length + 10);
foreach (var c in str)
{
switch (c)
{
case '\\':
builder.Append("\\\\");
break;
case '\r':
builder.Append("\\r");
break;
case '\n':
builder.Append("\\n");
break;
case '\t':
builder.Append("\\t");
break;
case '"':
builder.Append("\\\"");
break;
default:
builder.Append(c);
break;
}
}
return builder.ToString();
}
}
22 changes: 20 additions & 2 deletions TUnit.Core.SourceGenerator/CodeWriter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Text;

namespace TUnit.Core.SourceGenerator;
Expand All @@ -13,10 +14,18 @@ public class CodeWriter : ICodeWriter
internal int _indentLevel; // Keep old name for compatibility
private bool _isNewLine = true;

private static readonly ConcurrentDictionary<(string, int), string> _indentCache = new();

public CodeWriter(string indentString = " ", bool includeHeader = true)
{
_indentString = indentString;

for (var i = 0; i <= 10; i++)
{
var key = (_indentString, i);
_indentCache.TryAdd(key, string.Concat(Enumerable.Repeat(_indentString, i)));
}

if (includeHeader)
{
_builder.AppendLine("// <auto-generated/>");
Expand All @@ -26,7 +35,7 @@ public CodeWriter(string indentString = " ", bool includeHeader = true)
}
else
{
_isNewLine = true; // Fix: Always start at new line state for proper indentation
_isNewLine = true;
}
}

Expand All @@ -39,6 +48,15 @@ public ICodeWriter SetIndentLevel(int level)
return this;
}

/// <summary>
/// Gets the cached indentation string for the specified level, building it if necessary.
/// </summary>
private string GetIndentation(int level)
{
var key = (_indentString, level);
return _indentCache.GetOrAdd(key, static k => string.Concat(Enumerable.Repeat(k.Item1, k.Item2)));
}

/// <summary>
/// Appends text to the current line, applying indentation if at the start of a new line.
/// </summary>
Expand All @@ -51,7 +69,7 @@ public ICodeWriter Append(string text)

if (_isNewLine)
{
_builder.Append(string.Concat(Enumerable.Repeat(_indentString, _indentLevel)));
_builder.Append(GetIndentation(_indentLevel));
_isNewLine = false;
}
_builder.Append(text);
Expand Down
9 changes: 4 additions & 5 deletions TUnit.Core.SourceGenerator/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System.Text.RegularExpressions;

namespace TUnit.Core.SourceGenerator.Extensions;
namespace TUnit.Core.SourceGenerator.Extensions;

public static class StringExtensions
{
public static string ReplaceFirstOccurrence(this string source, string find, string replace)
{
var regex = new Regex(Regex.Escape(find));
return regex.Replace(source, replace, 1);
var place = source.IndexOf(find, StringComparison.Ordinal);

return place == -1 ? source : source.Remove(place, find.Length).Insert(place, replace);
}

public static string ReplaceLastOccurrence(this string source, string find, string replace)
Expand Down
38 changes: 27 additions & 11 deletions TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2421,6 +2421,22 @@ private static string GenerateTypeReference(INamedTypeSymbol typeSymbol, bool is
return $"typeof({fullyQualifiedName})";
}

private static string BuildTypeKey(IEnumerable<ITypeSymbol> types)
{
var typesList = types as IList<ITypeSymbol> ?? types.ToArray();
if (typesList.Count == 0)
{
return string.Empty;
}

var formattedTypes = new string[typesList.Count];
for (var i = 0; i < typesList.Count; i++)
{
formattedTypes[i] = typesList[i].ToDisplayString(DisplayFormats.FullyQualifiedGenericWithoutGlobalPrefix);
}
return string.Join(",", formattedTypes);
}

private static void GenerateGenericTypeInfo(CodeWriter writer, INamedTypeSymbol typeSymbol)
{
writer.AppendLine("GenericTypeInfo = new global::TUnit.Core.GenericTypeInfo");
Expand Down Expand Up @@ -2657,7 +2673,7 @@ private static void GenerateGenericTestWithConcreteTypes(
Array.Copy(classTypes, 0, combinedTypes, 0, classTypes.Length);
Array.Copy(methodTypes, 0, combinedTypes, classTypes.Length, methodTypes.Length);

var typeKey = string.Join(",", combinedTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(combinedTypes);

// Skip if we've already processed this type combination
if (!processedTypeCombinations.Add(typeKey))
Expand Down Expand Up @@ -2715,7 +2731,7 @@ private static void GenerateGenericTestWithConcreteTypes(

if (inferredTypes is { Length: > 0 })
{
var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(inferredTypes);

// Skip if we've already processed this type combination
if (!processedTypeCombinations.Add(typeKey))
Expand Down Expand Up @@ -2758,7 +2774,7 @@ private static void GenerateGenericTestWithConcreteTypes(
var inferredTypes = InferClassTypesFromMethodArguments(testMethod.TypeSymbol, testMethod.MethodSymbol, methodArgAttr, compilation);
if (inferredTypes is { Length: > 0 })
{
var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(inferredTypes);

// Skip if we've already processed this type combination
if (!processedTypeCombinations.Add(typeKey))
Expand Down Expand Up @@ -2795,7 +2811,7 @@ private static void GenerateGenericTestWithConcreteTypes(
var inferredTypes = InferTypesFromDataSourceAttribute(testMethod.MethodSymbol, dataSourceAttr);
if (inferredTypes is { Length: > 0 })
{
var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(inferredTypes);

// Skip if we've already processed this type combination
if (!processedTypeCombinations.Add(typeKey))
Expand Down Expand Up @@ -2834,7 +2850,7 @@ private static void GenerateGenericTestWithConcreteTypes(
var inferredTypes = InferTypesFromTypeInferringAttributes(testMethod.MethodSymbol);
if (inferredTypes is { Length: > 0 })
{
var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(inferredTypes);

// Skip if we've already processed this type combination
if (processedTypeCombinations.Add(typeKey))
Expand Down Expand Up @@ -2864,7 +2880,7 @@ private static void GenerateGenericTestWithConcreteTypes(
var inferredTypes = InferClassTypesFromMethodDataSource(compilation, testMethod, mdsAttr);
if (inferredTypes is { Length: > 0 })
{
var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(inferredTypes);

// Skip if we've already processed this type combination
if (processedTypeCombinations.Add(typeKey))
Expand All @@ -2888,7 +2904,7 @@ private static void GenerateGenericTestWithConcreteTypes(
var typedDataSourceInferredTypes = InferTypesFromTypedDataSourceForClass(testMethod.TypeSymbol, testMethod.MethodSymbol);
if (typedDataSourceInferredTypes is { Length: > 0 })
{
var typeKey = string.Join(",", typedDataSourceInferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(typedDataSourceInferredTypes);

// Skip if we've already processed this type combination
if (processedTypeCombinations.Add(typeKey))
Expand Down Expand Up @@ -2918,7 +2934,7 @@ private static void GenerateGenericTestWithConcreteTypes(
var inferredTypes = InferTypesFromMethodDataSource(compilation, testMethod, mdsAttr);
if (inferredTypes is { Length: > 0 })
{
var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(inferredTypes);

// Skip if we've already processed this type combination
if (processedTypeCombinations.Add(typeKey))
Expand Down Expand Up @@ -2965,7 +2981,7 @@ private static void GenerateGenericTestWithConcreteTypes(
{
// Combine class types and method types
var combinedTypes = classInferredTypes.Concat(methodInferredTypes).ToArray();
var typeKey = string.Join(",", combinedTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(combinedTypes);

// Skip if we've already processed this type combination
if (processedTypeCombinations.Add(typeKey))
Expand All @@ -2986,7 +3002,7 @@ private static void GenerateGenericTestWithConcreteTypes(
else
{
// For non-generic methods, just use class types
var typeKey = string.Join(",", classInferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(classInferredTypes);

// Skip if we've already processed this type combination
if (processedTypeCombinations.Add(typeKey))
Expand Down Expand Up @@ -3111,7 +3127,7 @@ private static void GenerateGenericTestWithConcreteTypes(
if (typeArgs.Count > 0)
{
var inferredTypes = typeArgs.ToArray();
var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")));
var typeKey = BuildTypeKey(inferredTypes);

// Skip if we've already processed this type combination
if (processedTypeCombinations.Add(typeKey))
Expand Down
27 changes: 22 additions & 5 deletions TUnit.Core/DataSources/TestDataFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ public static string FormatArguments(object?[] arguments, List<Func<object?, str
return string.Empty;
}

var formattedArgs = arguments.Select(arg => ArgumentFormatter.Format(arg, formatters)).ToArray();
var formattedArgs = new string[arguments.Length];
for (var i = 0; i < arguments.Length; i++)
{
formattedArgs[i] = ArgumentFormatter.Format(arguments[i], formatters);
}
return string.Join(", ", formattedArgs);
}

Expand All @@ -40,8 +44,11 @@ public static string FormatArguments(object?[] arguments)
return string.Empty;
}

var formattedArgs = arguments.Select(arg => ArgumentFormatter.Format(arg, [
])).ToArray();
var formattedArgs = new string[arguments.Length];
for (var i = 0; i < arguments.Length; i++)
{
formattedArgs[i] = ArgumentFormatter.Format(arguments[i], []);
}
return string.Join(", ", formattedArgs);
}

Expand Down Expand Up @@ -77,7 +84,12 @@ public static string CreateGenericDisplayName(TestMetadata metadata, Type[] gene

if (genericTypes.Length > 0)
{
var genericPart = string.Join(", ", genericTypes.Select(GetSimpleTypeName));
var genericTypeNames = new string[genericTypes.Length];
for (var i = 0; i < genericTypes.Length; i++)
{
genericTypeNames[i] = GetSimpleTypeName(genericTypes[i]);
}
var genericPart = string.Join(", ", genericTypeNames);
testName = $"{testName}<{genericPart}>";
}

Expand Down Expand Up @@ -105,7 +117,12 @@ private static string GetSimpleTypeName(Type type)
}

var genericArgs = type.GetGenericArguments();
var genericArgsText = string.Join(", ", genericArgs.Select(GetSimpleTypeName));
var genericArgNames = new string[genericArgs.Length];
for (var i = 0; i < genericArgs.Length; i++)
{
genericArgNames[i] = GetSimpleTypeName(genericArgs[i]);
}
var genericArgsText = string.Join(", ", genericArgNames);

return $"{genericTypeName}<{genericArgsText}>";
}
Expand Down
27 changes: 23 additions & 4 deletions TUnit.Core/GenericTestMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecut
// Determine the concrete types from the test arguments
var inferredTypes = InferTypesFromArguments(context.Arguments, metadata);

string typeKey;
if (inferredTypes is { Length: > 0 })
{
// Create a key from the inferred types - must match source generator format
var typeKey = string.Join(",", inferredTypes.Select(t => t.FullName ?? t.Name));
var typeNames = new string[inferredTypes.Length];
for (var i = 0; i < inferredTypes.Length; i++)
{
typeNames[i] = inferredTypes[i].FullName ?? inferredTypes[i].Name;
}
typeKey = string.Join(",", typeNames);

// Find the matching concrete instantiation
if (genericMetadata.ConcreteInstantiations.TryGetValue(typeKey, out var concreteMetadata))
Expand All @@ -43,12 +49,16 @@ public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecut
return concreteMetadata.CreateExecutableTestFactory(context, concreteMetadata);
}
}
else
{
typeKey = "unknown";
}

// If we couldn't find a match but have instantiations, throw an error
var availableKeys = string.Join(", ", genericMetadata.ConcreteInstantiations.Keys);
throw new InvalidOperationException(
$"No concrete instantiation found for generic method {metadata.TestMethodName} " +
$"with type arguments: {(inferredTypes?.Length > 0 ? string.Join(",", inferredTypes.Select(t => t.FullName ?? t.Name)) : "unknown")}. " +
$"with type arguments: {typeKey}. " +
$"Available: {availableKeys}");
}

Expand Down Expand Up @@ -130,8 +140,17 @@ public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecut
}

// Determine if the test method has a CancellationToken parameter
var parameterTypes = metadata.MethodMetadata.Parameters.Select(p => p.Type).ToArray();
var hasCancellationToken = parameterTypes.Any(t => t == typeof(CancellationToken));
var parameters = metadata.MethodMetadata.Parameters;
var parameterTypes = new Type[parameters.Length];
var hasCancellationToken = false;
for (var i = 0; i < parameters.Length; i++)
{
parameterTypes[i] = parameters[i].Type;
if (parameters[i].Type == typeof(CancellationToken))
{
hasCancellationToken = true;
}
}

if (hasCancellationToken)
{
Expand Down
8 changes: 6 additions & 2 deletions TUnit.Core/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,12 @@ public string GetDisplayName()
return TestName;
}

var arguments = string.Join(", ", TestDetails.TestMethodArguments
.Select(arg => ArgumentFormatter.Format(arg, ArgumentDisplayFormatters)));
var formattedArgs = new string[TestDetails.TestMethodArguments.Length];
for (var i = 0; i < TestDetails.TestMethodArguments.Length; i++)
{
formattedArgs[i] = ArgumentFormatter.Format(TestDetails.TestMethodArguments[i], ArgumentDisplayFormatters);
}
var arguments = string.Join(", ", formattedArgs);

if (string.IsNullOrEmpty(arguments))
{
Expand Down
Loading
Loading