Enumerable is not reused for assertion check and message
See original GitHub issueBefore you file a bug, have you:
- Tried upgrading to newest version of Fluent Assertions, to see if your issue has already been resolved and released? - yes
- Checked existing open and closed issues, to see if the issue has already been reported? - yes could not find it
- Tried reproducing your problem in a new isolated project? - yes
- Read the documentation? - yes
- Considered if this is a general question and not a bug?. For general questions please use StackOverflow. - yes, seems like a bug
Description
Using collection assertion on IEnumerable leads to enumerating the collection twice. Once for the actual assertion check and second time for formatting.
When items are actively added to the enumerable, you might end up with assertion failure that lists correct collection items. This makes the assertion message incorrect and confusing.
E.g. (added newlines for readability):
FluentAssertions.Execution.AssertionFailedException: Expected enumerable
{"a", "b", "c"}
to contain items
{"a", "b", "c"}
in order, but "c" (index 2) did not appear (in the right order).
at FluentAssertions.Execution.FallbackTestFramework.Throw(String message)
I saw this in real system, where one side is outputting messages and I am adding them into a list as they come, then using LINQ Select to select just the names of those messages and comparing them to what I expect. The server outputs last message and closes, and somehow just at the right time FluentAssertions check the collection, and just before formatting, the last message is output, and I see the error above.
Complete minimal example reproducing the issue
Complete means the code snippet can be copied into a unit test method in a fresh C# project and run. Minimal means it is stripped from code not related to reproducing the issue.
The following example outputs 2 items when first asked for the enumerator, and then all items when asked second time. Simulating what happens when item is added to the enumerable just at the right time. It also writes to console when it is asked for enumerator. E.g.
using FluentAssertions;
using System.Collections;
namespace ConsoleApp38
{
internal class DoublyEnumerable : IEnumerable<string>
{
private readonly List<string> _collection;
private bool _called;
public DoublyEnumerable()
{
_collection = new List<string> { "a", "b", "c" };
}
public IEnumerator<string> GetEnumerator()
{
Console.WriteLine("GetEnumerator called");
if (!_called)
{
_called = true;
return _collection.Take(2).GetEnumerator();
}
return _collection.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
internal class Program
{
static void Main(string[] args)
{
try {
var enumerable = new DoublyEnumerable();
enumerable.Should().ContainInOrder(new[] { "a", "b", "c" });
Console.WriteLine("it passed");
}
catch (Exception ex){
Console.WriteLine(ex);
}
}
}
}
Output:
GetEnumerator called
GetEnumerator called
FluentAssertions.Execution.AssertionFailedException: Expected enumerable {"a", "b", "c"}
to contain items {"a", "b", "c"} in order, but "c" (index 2) did not appear (in the right order).
at FluentAssertions.Execution.FallbackTestFramework.Throw(String message)
at FluentAssertions.Execution.TestFrameworkProvider.Throw(String message)
at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message)
at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args)
at FluentAssertions.Collections.GenericCollectionAssertions`3.ContainInOrder(IEnumerable`1 expected, String because, Object[] becauseArgs)
at FluentAssertions.Collections.GenericCollectionAssertions`3.ContainInOrder(T[] expected)
at ConsoleApp38.Program.Main(String[] args) in S:\t\ConsoleApp38\ConsoleApp38\Program.cs:line 39
Expected behavior:
I would expect the assertion to save that data it saw when checking for success or failure, and re-use that for the message formatter.
Actual behavior:
What I described above. Assertion fails on older data, and prints newer data which are correct and leave me stumped about what is wrong.
Versions
- Which version of Fluent Assertions are you using? 6.7.0
- Which .NET runtime and version are you targeting? E.g. .NET framework 4.6.1 or .NET Core 2.1. Can repro on both net6 and net48.
Additional Information
Issue Analytics
- State:
- Created a year ago
- Comments:5 (4 by maintainers)
Top GitHub Comments
Here’s a prototype of what I was imagining https://github.com/jnyrup/fluentassertions/tree/issue2000_materializing_enumerable
I don’t imagine we want to guard against concurrency.
In terms of efficiency I have an idea about wrapping non-materialized enumerables into a lazily materializing collection. One of the cases to take into account is that we e.g. don’t want to enumerate an infinite sequence to tell if it is empty or not.