Skip to content
Closed
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
19 changes: 14 additions & 5 deletions src/Opc.Ua.Types/BuiltIn/ArrayOf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -924,17 +924,26 @@ public static ArrayOf<T> ToArrayOf<T>(this IEnumerable<T>? values)
#if NET8_0_OR_GREATER
if (values.TryGetNonEnumeratedCount(out int count))
{
if (count == 0)
{
return [];
}
// TryGetNonEnumeratedCount reads a live Count (e.g. a
// ConcurrentQueue<T> shared with a producer), which is not
// atomic with the enumeration below: the source can grow or
// shrink before or while it is enumerated. Grow the buffer
// rather than index past it, and trim if it enumerated short.
var copy = new T[count];
int index = 0;
foreach (T item in values)
{
if (index == copy.Length)
{
Array.Resize(ref copy, Math.Max(copy.Length * 2, 4));
}
copy[index++] = item;
}
return new(copy);
if (index != copy.Length)
{
Array.Resize(ref copy, index);
}
return index == 0 ? [] : new(copy);
}
#endif
#pragma warning disable RCS1151 // Cast explicit to avoid covariant conversion
Expand Down
71 changes: 71 additions & 0 deletions tests/Opc.Ua.Types.Tests/BuiltIn/ArrayOfTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,77 @@ public void ToArrayOfFromEmptyIEnumerableTest()
Assert.That(arrayOf.IsEmpty, Is.True);
}

[Test]
public void ToArrayOfFromIEnumerableThatGrowsPastItsReportedCountTest()
{
// TryGetNonEnumeratedCount reads a live Count that is not atomic
// with the enumeration, e.g. a ConcurrentQueue<T> a producer
// enqueues to between the count check and the foreach below.
int[] items = [1, 2, 3, 4, 5];
var enumerable = new StaleCountCollection<int>(reportedCount: 2, items);

var arrayOf = enumerable.ToArrayOf();

Assert.That(arrayOf.Count, Is.EqualTo(items.Length));
Assert.That(arrayOf.Span.ToArray(), Is.EquivalentTo(items));
}

[Test]
public void ToArrayOfFromIEnumerableThatShrinksBelowItsReportedCountTest()
{
int[] items = [1, 2];
var enumerable = new StaleCountCollection<int>(reportedCount: 5, items);

var arrayOf = enumerable.ToArrayOf();

Assert.That(arrayOf.Count, Is.EqualTo(items.Length));
Assert.That(arrayOf.Span.ToArray(), Is.EquivalentTo(items));
}

/// <summary>
/// An <see cref="ICollection"/> whose <see cref="Count"/> does not match
/// the number of elements <see cref="GetEnumerator"/> yields, mirroring
/// the interfaces <see cref="System.Collections.Concurrent.ConcurrentQueue{T}"/>
/// implements (<see cref="ICollection"/> and <see cref="IReadOnlyCollection{T}"/>,
/// not the generic <see cref="ICollection{T}"/>) so both the count-based
/// fast path of
/// <see cref="System.Linq.Enumerable.TryGetNonEnumeratedCount{TSource}"/>
/// and the plain enumeration fallback on earlier target frameworks are
/// exercised the same way a live producer/consumer queue would.
/// </summary>
/// <typeparam name="T">Type of the element in the collection.</typeparam>
private sealed class StaleCountCollection<T> : ICollection, IEnumerable<T>, IReadOnlyCollection<T>
{
private readonly T[] m_items;

public StaleCountCollection(int reportedCount, T[] items)
{
Count = reportedCount;
m_items = items;
}

public int Count { get; }

public bool IsSynchronized => false;

public object SyncRoot => this;

public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)m_items).GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

void ICollection.CopyTo(Array array, int index)
{
throw new NotSupportedException();
}
}

[Test]
public void ToArrayOfFromArrayTest()
{
Expand Down