Limit formatting to a maximum number of lines by dennisdoomen · Pull Request #1469 · fluentassertions/fluentassertions · GitHub
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
6 changes: 6 additions & 0 deletions Src/FluentAssertions/AssertionOptions.cs
6 changes: 3 additions & 3 deletions Src/FluentAssertions/CallerIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ internal static bool OnlyOneFluentAssertionScopeOnCallStack()
int startOfSecondFluentAssertionsScopeStackFrameIndex = Array.FindIndex(
allStackFrames,
startIndex: firstNonFluentAssertionsStackFrameIndex + 1,
frame => IsCurrentAssembly(frame));
IsCurrentAssembly);

return startOfSecondFluentAssertionsScopeStackFrameIndex < 0;
}
Expand All @@ -148,7 +148,7 @@ private static bool IsDynamic(StackFrame frame)

private static bool IsCurrentAssembly(StackFrame frame)
{
return frame.GetMethod().DeclaringType.Assembly == typeof(CallerIdentifier).Assembly;
return frame.GetMethod().DeclaringType?.Assembly == typeof(CallerIdentifier).Assembly;
}

private static bool IsDotNet(StackFrame frame)
Expand All @@ -162,7 +162,7 @@ private static bool IsDotNet(StackFrame frame)

private static bool IsCompilerServices(StackFrame frame)
{
return frame.GetMethod().DeclaringType.Namespace == "System.Runtime.CompilerServices";
return frame.GetMethod().DeclaringType?.Namespace == "System.Runtime.CompilerServices";
}

private static string ExtractVariableNameFrom(StackFrame frame)
Expand Down
145 changes: 145 additions & 0 deletions Src/FluentAssertions/Common/Iterator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace FluentAssertions.Common
{
/// <summary>
/// A smarter enumerator that can provide information about the relative location (current, first, last)
/// of the current item within the collection without unnecessarily iterating the collection.
/// </summary>
internal class Iterator<T> : IEnumerator<T>
{
private const int InitialIndex = -1;
private readonly IEnumerable<T> enumerable;
private readonly int? maxItems;
private IEnumerator<T> enumerator;
private T current;
private T next;

private bool hasNext;
private bool hasCurrent;

private bool hasCompleted;

public Iterator(IEnumerable<T> enumerable, int maxItems = int.MaxValue)
{
this.enumerable = enumerable;
this.maxItems = maxItems;

Reset();
}

public void Reset()
{
Index = InitialIndex;

enumerator = enumerable.GetEnumerator();
hasCurrent = false;
hasNext = false;
hasCompleted = false;
current = default;
next = default;
}

public int Index { get; private set; }

public bool IsFirst => Index == 0;

public bool IsLast => (hasCurrent && !hasNext) || HasReachedMaxItems;

object IEnumerator.Current => Current;

public T Current
{
get
{
if (!hasCurrent)
{
throw new InvalidOperationException($"Please call {nameof(MoveNext)} first");
}

return current;
}

private set
{
current = value;
hasCurrent = true;
}
}

public bool MoveNext()
{
if (!hasCompleted)
{
if (FetchCurrent())
{
PrefetchNext();
return true;
}
}

hasCompleted = true;
return false;
}

private bool FetchCurrent()
{
if (hasNext && !HasReachedMaxItems)
{
Current = next;
Index++;

return true;
}

if (enumerator.MoveNext() && !HasReachedMaxItems)
{
Current = enumerator.Current;
Index++;

return true;
}
else
{
hasCompleted = true;
return false;
}
}

public bool HasReachedMaxItems => Index == maxItems;

private void PrefetchNext()
{
if (enumerator.MoveNext())
{
next = enumerator.Current;
hasNext = true;
}
else
{
next = default;
hasNext = false;
}
}

public bool IsEmpty
{
get
{
if (!hasCurrent && !hasCompleted)
{
throw new InvalidOperationException($"Please call {nameof(MoveNext)} first");
}

return Index == InitialIndex;
}
}

public void Dispose()
{
enumerator.Dispose();
}
}
}
22 changes: 17 additions & 5 deletions Src/FluentAssertions/Execution/AssertionScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Threading;
using FluentAssertions.Common;
using FluentAssertions.Formatting;

namespace FluentAssertions.Execution
{
Expand All @@ -17,11 +18,11 @@ public sealed class AssertionScope : IAssertionScope
{
#region Private Definitions

private readonly FormattingOptions formattingOptions = AssertionOptions.FormattingOptions.Clone();
private readonly IAssertionStrategy assertionStrategy;
private readonly ContextDataItems contextData = new();

private Func<string> reason;
private bool useLineBreaks;

private static readonly AsyncLocal<AssertionScope> CurrentScope = new();
private AssertionScope parent;
Expand Down Expand Up @@ -121,16 +122,27 @@ public static AssertionScope Current
private set => SetCurrentAssertionScope(value);
}

/// <summary>
/// Forces the formatters that support it to add the necessary line breaks.
/// </summary>
/// <remarks>
/// This is just shorthand for modifying the <see cref="FormattingOptions"/> property.
/// </remarks>
public AssertionScope UsingLineBreaks
{
get
{
useLineBreaks = true;
formattingOptions.UseLineBreaks = true;
return this;
}
}

public bool Succeeded
/// <summary>
/// Exposes the options the scope will use for formatting objects in case an assertion fails.
/// </summary>
public FormattingOptions FormattingOptions => formattingOptions;

internal bool Succeeded
{
get => succeeded == true;
}
Expand Down Expand Up @@ -184,7 +196,7 @@ public AssertionScope WithExpectation(string message, params object[] args)
Func<string> localReason = reason;
expectation = () =>
{
var messageBuilder = new MessageBuilder(useLineBreaks);
var messageBuilder = new MessageBuilder(formattingOptions);
string reason = localReason?.Invoke() ?? string.Empty;
string identifier = GetIdentifier();

Expand Down Expand Up @@ -240,7 +252,7 @@ public Continuation FailWith(Func<FailReason> failReasonFunc)
return FailWith(() =>
{
string localReason = reason?.Invoke() ?? string.Empty;
var messageBuilder = new MessageBuilder(useLineBreaks);
var messageBuilder = new MessageBuilder(formattingOptions);
string identifier = GetIdentifier();
FailReason failReason = failReasonFunc();
string result = messageBuilder.Build(failReason.Message, failReason.Args, localReason, contextData, identifier, fallbackIdentifier);
Expand Down
13 changes: 6 additions & 7 deletions Src/FluentAssertions/Execution/MessageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ namespace FluentAssertions.Execution
/// </summary>
internal class MessageBuilder
{
#region Private Definitions
private readonly FormattingOptions formattingOptions;

private readonly bool useLineBreaks;
#region Private Definitions

private readonly char[] blanks = { '\r', '\n', ' ', '\t' };

#endregion

public MessageBuilder(bool useLineBreaks)
public MessageBuilder(FormattingOptions formattingOptions)
{
this.useLineBreaks = useLineBreaks;
this.formattingOptions = formattingOptions;
}

// SMELL: Too many parameters.
Expand Down Expand Up @@ -89,10 +89,9 @@ private static string SubstituteContextualTags(string message, ContextDataItems

private string FormatArgumentPlaceholders(string failureMessage, object[] failureArgs)
{
string[] values = failureArgs.Select(a => Formatter.ToString(a, useLineBreaks)).ToArray();
string formattedMessage = string.Format(CultureInfo.InvariantCulture, failureMessage, values);
string[] values = failureArgs.Select(a => Formatter.ToString(a, formattingOptions)).ToArray();

return formattedMessage;
return string.Format(CultureInfo.InvariantCulture, failureMessage, values);
}

private string SanitizeReason(string reason)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Globalization;
using System.Text;
using static System.FormattableString;

namespace FluentAssertions.Formatting
{
Expand All @@ -18,27 +17,24 @@ public bool CanHandle(object value)
return value is AggregateException;
}

/// <inheritdoc />
public string Format(object value, FormattingContext context, FormatChild formatChild)
public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild)
{
var exception = (AggregateException)value;
if (exception.InnerExceptions.Count == 1)
{
return "(aggregated) " + formatChild("inner", exception.InnerException);
formattedGraph.AddFragment("(aggregated) ");

formatChild("inner", exception.InnerException, formattedGraph);
}
else
{
var builder = new StringBuilder();

builder.AppendFormat(CultureInfo.InvariantCulture, "{0} (aggregated) exceptions:\n", exception.InnerExceptions.Count);
formattedGraph.AddLine(Invariant($"{exception.InnerExceptions.Count} (aggregated) exceptions:"));

foreach (Exception innerException in exception.InnerExceptions)
{
builder.AppendLine();
builder.AppendLine(formatChild("InnerException", innerException));
formattedGraph.AddLine(string.Empty);
formatChild("InnerException", innerException, formattedGraph);
}

return builder.ToString();
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions Src/FluentAssertions/Formatting/AttributeBasedFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,13 @@ private static bool IsScanningEnabled
get { return Configuration.Current.ValueFormatterDetectionMode != ValueFormatterDetectionMode.Disabled; }
}

/// <inheritdoc />
public string Format(object value, FormattingContext context, FormatChild formatChild)
public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild)
{
MethodInfo method = GetFormatter(value);

object[] parameters = new[] { value };
object[] parameters = new[] { value, formattedGraph };

return (string)method.Invoke(null, parameters);
method.Invoke(null, parameters);
}

private MethodInfo GetFormatter(object value)
Expand Down Expand Up @@ -86,10 +85,11 @@ from type in Services.Reflector.GetAllTypesFromAppDomain(Applicable)
where type is not null
from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)
where method.IsStatic
where method.ReturnType == typeof(string)
where method.ReturnType == typeof(void)
where method.IsDecoratedWithOrInherit<ValueFormatterAttribute>()
where method.GetParameters().Length == 1
select new { Type = method.GetParameters().Single().ParameterType, Method = method } into formatter
let methodParameters = method.GetParameters()
where methodParameters.Length == 2
select new { Type = methodParameters.First().ParameterType, Method = method } into formatter
group formatter by formatter.Type into formatterGroup
select formatterGroup.First();

Expand Down
5 changes: 2 additions & 3 deletions Src/FluentAssertions/Formatting/ByteValueFormatter.cs
Loading