Skip to content
Open
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
36 changes: 31 additions & 5 deletions src/Allure.Net.Commons/AllureLifecycle.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -33,15 +32,18 @@ namespace Allure.Net.Commons;
public class AllureLifecycle
{
private readonly Dictionary<Type, ITypeFormatter> typeFormatters = new();
private readonly HierarchicalTypeFormatterLookup typeFormatterLookup;
private static readonly Lazy<AllureLifecycle> instance =
new(Initialize);

/// <summary>
/// The list of the currently registered formatters used by Allure to
/// convert test and step arguments to strings.
/// convert test and step arguments to strings. Looking up a type that
/// has no formatter registered for it directly falls back to its
/// generic type definition, the interfaces it implements, and its base
/// classes, in that order.
/// </summary>
public IReadOnlyDictionary<Type, ITypeFormatter> TypeFormatters =>
new ReadOnlyDictionary<Type, ITypeFormatter>(typeFormatters);
public IReadOnlyDictionary<Type, ITypeFormatter> TypeFormatters => typeFormatterLookup;

readonly AsyncLocal<AllureContext> context = new();

Expand Down Expand Up @@ -106,6 +108,7 @@ Func<AllureTestPlan> testPlanFactory
AllureConfiguration = AllureConfiguration.ReadFromJObject(config);
writer = writerFactory(AllureConfiguration);
lazyTestPlan = new(testPlanFactory);
typeFormatterLookup = new(typeFormatters);
}

/// <summary>
Expand All @@ -119,6 +122,7 @@ public AllureLifecycle(AllureConfiguration config, IAllureResultsWriter writer)
this.AllureConfiguration = config;
this.writer = writer;
this.lazyTestPlan = new(AllureTestPlan.FromEnvironment);
this.typeFormatterLookup = new(this.typeFormatters);
}

/// <summary>
Expand All @@ -138,6 +142,7 @@ Dictionary<Type, ITypeFormatter> typeFormatters
this.writer = writer;
this.lazyTestPlan = new(AllureTestPlan.FromEnvironment);
this.typeFormatters = typeFormatters;
this.typeFormatterLookup = new(this.typeFormatters);
}

/// <summary>
Expand Down Expand Up @@ -176,8 +181,29 @@ Dictionary<Type, ITypeFormatter> typeFormatters
public void AddTypeFormatter<T>(TypeFormatter<T> typeFormatter) =>
AddTypeFormatterImpl(typeof(T), typeFormatter);

private void AddTypeFormatterImpl(Type type, ITypeFormatter formatter) =>
/// <summary>
/// Registers a type formatter to be used when converting a test's or
/// step's argument to the string that will be included in the Allure
/// report. Unlike <see cref="AddTypeFormatter{T}(TypeFormatter{T})" />,
/// this overload accepts an open generic type definition (e.g.
/// <c>typeof(List&lt;&gt;)</c>), which cannot be expressed as a type
/// argument.
/// </summary>
/// <param name="type">
/// The type that the formatter converts. An argument is matched against
/// this type exactly, or, failing that, against its generic type
/// definition, the interfaces it implements, and its base classes, in
/// that order.
/// </param>
/// <param name="formatter">The formatter instance.</param>
public void AddTypeFormatter(Type type, ITypeFormatter formatter) =>
AddTypeFormatterImpl(type, formatter);

private void AddTypeFormatterImpl(Type type, ITypeFormatter formatter)
{
typeFormatters[type] = formatter;
typeFormatterLookup.Invalidate();
}

/// <summary>
/// Binds the provided value as the current Allure context and executes
Expand Down
4 changes: 4 additions & 0 deletions src/Allure.Net.Commons/Functions/FormatFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public static string Format(object? value)
/// if JSON serialization failed.
/// The serializer skips fields that contain loop references
/// and fields that could not be serialized
/// Passing <see cref="AllureLifecycle.TypeFormatters" /> matches a
/// formatter by the value's exact type or, failing that, by its
/// generic type definition, the interfaces it implements, or its
/// base classes. A plain dictionary matches by exact type only.
/// </summary>
public static string Format(
object? value,
Expand Down
132 changes: 132 additions & 0 deletions src/Allure.Net.Commons/HierarchicalTypeFormatterLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;

#nullable enable

namespace Allure.Net.Commons;

/// <summary>
/// Wraps a dictionary of explicitly registered type formatters and resolves
/// a formatter for a given runtime type by walking its type hierarchy when
/// no formatter is registered for the exact type.
/// </summary>
/// <remarks>
/// Resolution order:
/// <list type="number">
/// <item>The exact type.</item>
/// <item>The type's generic type definition, if it is a closed generic type.</item>
/// <item>Each interface the type implements, in the order returned by <see cref="Type.GetInterfaces" />.</item>
/// <item>The generic type definition of each such interface.</item>
/// <item>Each base class, from the closest to the furthest.</item>
/// <item>The generic type definition of each such base class.</item>
/// </list>
/// Resolved results are cached per runtime type. The cache is invalidated
/// whenever a new formatter is registered.
/// </remarks>
internal sealed class HierarchicalTypeFormatterLookup : IReadOnlyDictionary<Type, ITypeFormatter>
{
private readonly Dictionary<Type, ITypeFormatter> registered;
private readonly ConcurrentDictionary<Type, ITypeFormatter?> resolveCache = new();

public HierarchicalTypeFormatterLookup(Dictionary<Type, ITypeFormatter> registered)
{
this.registered = registered;
}

/// <summary>
/// Drops all cached resolution results. Must be called whenever the
/// underlying registered-formatters dictionary is mutated.
/// </summary>
public void Invalidate() => resolveCache.Clear();

public bool TryGetValue(Type key, out ITypeFormatter value)
{
var resolved = resolveCache.GetOrAdd(key, Resolve);
value = resolved!;
return resolved is not null;
}

private ITypeFormatter? Resolve(Type type)
{
if (registered.TryGetValue(type, out var exact))
{
return exact;
}

if (type.IsGenericType &&
registered.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition))
{
return genericDefinition;
}

var interfaces = type.GetInterfaces();

foreach (var i in interfaces)
{
if (registered.TryGetValue(i, out var iface))
{
return iface;
}
}

foreach (var i in interfaces)
{
if (i.IsGenericType &&
registered.TryGetValue(i.GetGenericTypeDefinition(), out var ifaceGenericDefinition))
{
return ifaceGenericDefinition;
}
}

for (var b = type.BaseType; b is not null; b = b.BaseType)
{
if (registered.TryGetValue(b, out var baseExact))
{
return baseExact;
}
}

for (var b = type.BaseType; b is not null; b = b.BaseType)
{
if (b.IsGenericType &&
registered.TryGetValue(b.GetGenericTypeDefinition(), out var baseGenericDefinition))
{
return baseGenericDefinition;
}
}

return null;
}

/// <summary>
/// The explicitly registered types. Unlike <see cref="TryGetValue" />,
/// this doesn't include every type that could be resolved through them
/// (e.g., subclasses or implementors).
/// </summary>
public IEnumerable<Type> Keys => registered.Keys;

/// <summary>
/// The formatters registered for the types in <see cref="Keys" />, in
/// the same order.
/// </summary>
public IEnumerable<ITypeFormatter> Values => registered.Values;

/// <summary>
/// The number of explicitly registered types. See <see cref="Keys" />.
/// </summary>
public int Count => registered.Count;

public ITypeFormatter this[Type key] =>
TryGetValue(key, out var value)
? value
: throw new KeyNotFoundException($"No type formatter is registered for {key}.");

public bool ContainsKey(Type key) => TryGetValue(key, out _);

public IEnumerator<KeyValuePair<Type, ITypeFormatter>> GetEnumerator() =>
registered.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Loading
Loading