Skip to content

Commit

Permalink
🐛 Fix nested enumeration of container elements
Browse files Browse the repository at this point in the history
When a visitor enumerates elements while an outside enumeration is still happening, elements are missed because the cache is still being built.

Fixes #19.
  • Loading branch information
kzu committed Jan 28, 2021
1 parent 73f09ea commit 66e1214
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 24 deletions.
71 changes: 71 additions & 0 deletions src/NuDoq.Tests/ElementTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NuDoq;
using Xunit;
using Xunit.Abstractions;

namespace NuDoq
{
public class ElementTests
{
readonly ITestOutputHelper output;

public ElementTests(ITestOutputHelper output) => this.output = output;

[Fact]
public void when_enumerating_elements_then_can_list_twice_enumerates_once()
{
var enumerations = 0;

IEnumerable<Element> GetElements()
{
yield return new Text("foo");
yield return new Text("bar");
enumerations++;
};

var element = new Summary(GetElements(), new Dictionary<string, string>());

var first = element.Elements.Count();
var second = element.Elements.Count();

Assert.Contains(element.Elements.OfType<Text>(), e => e.Content == "foo");
Assert.Contains(element.Elements.OfType<Text>(), e => e.Content == "foo");
Assert.Contains(element.Elements.OfType<Text>(), e => e.Content == "bar");
Assert.Contains(element.Elements.OfType<Text>(), e => e.Content == "bar");
}

/// <remarks>
/// <list type="bullet">
/// <item>foo</item>
/// <item>bar</item>
/// <item>baz</item>
/// </list>
/// </remarks>
[Fact]
public void when_enumerating_elements_then_visit_twice()
{
var member = DocReader.Read(Assembly.GetExecutingAssembly());
var method = member.Elements.OfType<Method>()
.Where(x => x.Elements.OfType<TypeParam>().Count() > 2)
.FirstOrDefault();

Assert.NotNull(method);

List? currentList = null;
var count = 0;

var visitor = new DelegateVisitor(new VisitorDelegates
{
VisitList = (List list) => currentList = list,
VisitItem = (Item item) => { output.WriteLine(currentList.Elements.Count().ToString()); count++; },
});

member.Accept(visitor);

Assert.Equal(3, count);
}
}
}
26 changes: 2 additions & 24 deletions src/NuDoq/CachedEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,12 @@ static class CachedEnumerable

class CachedEnumerableImpl<T> : IEnumerable<T>
{
IEnumerator<T>? enumerator;
readonly IEnumerable<T> enumerable;
readonly List<T> cache = new List<T>();
List<T>? cache;

public CachedEnumerableImpl(IEnumerable<T> enumerable) => this.enumerable = enumerable;

public IEnumerator<T> GetEnumerator()
{
// First time around, there will be nothing in
// this cache.
foreach (var item in cache)
yield return item;

// First time we'll get the enumerator, only
// once. Next time, it will already have a value
// and so we won't enumerate twice ever.
if (enumerator == null)
enumerator = enumerable.GetEnumerator();

// First time around, we'll loop until we're done.
// Next time it's enumerated, this enumerator will
// return false from MoveNext right-away.
while (enumerator.MoveNext())
{
cache.Add(enumerator.Current);
yield return enumerator.Current;
}
}
public IEnumerator<T> GetEnumerator() => (cache ??= new List<T>(enumerable)).GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Expand Down

0 comments on commit 66e1214

Please sign in to comment.