From 4f341fc2d50d86aa13e8f46e8e44904f3716dfca Mon Sep 17 00:00:00 2001 From: Ian Griffiths Date: Wed, 19 Jun 2024 13:23:00 +0100 Subject: [PATCH] Fix Union Dispose bug #2112 (#2131) * Fix Union Dispose bug #2112 * Update Ix build to use .NET 8.0 SDK * Align ref project names with assembly names (This seems to have become necessary in .NET SDK 8.0.) --- Ix.NET/Source/Ix.NET.sln | 6 +- .../System.Interactive.Providers.csproj | 2 +- .../System.Interactive.csproj | 2 +- .../System/Linq/Operators/Union.cs | 179 ++++++++++++++++++ .../System.Linq.Async.csproj | 2 +- .../System/Linq/Operators/Union.cs | 2 +- ...oj => System.Interactive.Providers.csproj} | 3 +- ...e.Ref.csproj => System.Interactive.csproj} | 1 - ...nc.Ref.csproj => System.Linq.Async.csproj} | 0 azure-pipelines.ix.yml | 10 +- 10 files changed, 195 insertions(+), 12 deletions(-) rename Ix.NET/Source/refs/System.Interactive.Providers.Ref/{System.Interactive.Providers.Ref.csproj => System.Interactive.Providers.csproj} (88%) rename Ix.NET/Source/refs/System.Interactive.Ref/{System.Interactive.Ref.csproj => System.Interactive.csproj} (91%) rename Ix.NET/Source/refs/System.Linq.Async.Ref/{System.Linq.Async.Ref.csproj => System.Linq.Async.csproj} (100%) diff --git a/Ix.NET/Source/Ix.NET.sln b/Ix.NET/Source/Ix.NET.sln index e95c5ad13c..32435c26dd 100644 --- a/Ix.NET/Source/Ix.NET.sln +++ b/Ix.NET/Source/Ix.NET.sln @@ -53,13 +53,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Refs", "Refs", "{A3D72E6E-4 refs\Directory.build.props = refs\Directory.build.props EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Interactive.Ref", "refs\System.Interactive.Ref\System.Interactive.Ref.csproj", "{2EC0C302-B029-4DDB-AC91-000BF11006AD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Interactive", "refs\System.Interactive.Ref\System.Interactive.csproj", "{2EC0C302-B029-4DDB-AC91-000BF11006AD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Interactive.Providers.Ref", "refs\System.Interactive.Providers.Ref\System.Interactive.Providers.Ref.csproj", "{5DF341BE-B369-4250-AFD4-604DE8C95E45}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Interactive.Providers", "refs\System.Interactive.Providers.Ref\System.Interactive.Providers.csproj", "{5DF341BE-B369-4250-AFD4-604DE8C95E45}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks.System.Interactive", "Benchmarks.System.Interactive\Benchmarks.System.Interactive.csproj", "{3285529A-8227-4D40-B524-1A1F919F0E7B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Linq.Async.Ref", "refs\System.Linq.Async.Ref\System.Linq.Async.Ref.csproj", "{1754B36C-D0DB-4E5D-8C30-1F116046DC0F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Linq.Async", "refs\System.Linq.Async.Ref\System.Linq.Async.csproj", "{1754B36C-D0DB-4E5D-8C30-1F116046DC0F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Linq.Async.SourceGenerator", "System.Linq.Async.SourceGenerator\System.Linq.Async.SourceGenerator.csproj", "{5C26D649-5ED4-49EE-AFBD-8FA8F12C4AE4}" EndProject diff --git a/Ix.NET/Source/System.Interactive.Providers/System.Interactive.Providers.csproj b/Ix.NET/Source/System.Interactive.Providers/System.Interactive.Providers.csproj index 93b9840fdc..9f17be765d 100644 --- a/Ix.NET/Source/System.Interactive.Providers/System.Interactive.Providers.csproj +++ b/Ix.NET/Source/System.Interactive.Providers/System.Interactive.Providers.csproj @@ -13,7 +13,7 @@ - + diff --git a/Ix.NET/Source/System.Interactive/System.Interactive.csproj b/Ix.NET/Source/System.Interactive/System.Interactive.csproj index 8badecf58b..44dbc3967a 100644 --- a/Ix.NET/Source/System.Interactive/System.Interactive.csproj +++ b/Ix.NET/Source/System.Interactive/System.Interactive.csproj @@ -10,7 +10,7 @@ - + diff --git a/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/Operators/Union.cs b/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/Operators/Union.cs index 8f84ef9729..3f4c0a9f73 100644 --- a/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/Operators/Union.cs +++ b/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/Operators/Union.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -162,6 +163,128 @@ public async Task Union_ToList() Assert.Equal(new[] { 1, 2, 3, 4, 5 }, (await res.ToListAsync()).OrderBy(x => x)); } + + [Fact] + public async Task Union_DisposesNotEmpty() + { + var e1 = new DisposalDetectingEnumerable(10, 2); + var e2 = new DisposalDetectingEnumerable(20, 2); + var res = e1.Union(e2).OrderBy(x => x); + + var e = res.GetAsyncEnumerator(); + await HasNextAsync(e, 10); + await HasNextAsync(e, 11); + await HasNextAsync(e, 20); + await HasNextAsync(e, 21); + await NoNextAsync(e); + + Assert.Single(e1.Enumerators); + Assert.Single(e2.Enumerators); + Assert.Equal([true, true], [e1.Enumerators[0].Disposed, e2.Enumerators[0].Disposed]); + } + + [Fact] + public async Task Union_DisposesFirstEmpty() + { + var e1 = new DisposalDetectingEnumerable(0, 0); + var e2 = new DisposalDetectingEnumerable(1, 1); + var res = e1.Union(e2); + + var e = res.GetAsyncEnumerator(); + await HasNextAsync(e, 1); + await NoNextAsync(e); + + Assert.Single(e1.Enumerators); + Assert.Single(e2.Enumerators); + Assert.Equal([true, true], [e1.Enumerators[0].Disposed, e2.Enumerators[0].Disposed]); + } + + [Fact] + public async Task Union_DisposesSecondOfTwoEmpty() + { + var e1 = new DisposalDetectingEnumerable(1, 1); + var e2 = new DisposalDetectingEnumerable(0, 0); + var res = e1.Union(e2); + + var e = res.GetAsyncEnumerator(); + await HasNextAsync(e, 1); + await NoNextAsync(e); + + Assert.Single(e1.Enumerators); + Assert.Single(e2.Enumerators); + Assert.Equal([true, true], [e1.Enumerators[0].Disposed, e2.Enumerators[0].Disposed]); + } + + [Fact] + public async Task Union_DisposesSecondOfThreeEmpty() + { + var e1 = new DisposalDetectingEnumerable(10, 1); + var e2 = new DisposalDetectingEnumerable(0, 0); + var e3 = new DisposalDetectingEnumerable(30, 1); + var res = e1.Union(e2).Union(e3); + + var e = res.GetAsyncEnumerator(); + await HasNextAsync(e, 10); + await HasNextAsync(e, 30); + await NoNextAsync(e); + + Assert.Single(e1.Enumerators); + Assert.Single(e2.Enumerators); + Assert.Single(e3.Enumerators); + Assert.Equal([true, true, true], [e1.Enumerators[0].Disposed, e2.Enumerators[0].Disposed, e3.Enumerators[0].Disposed]); + } + + [Fact] + public async Task Union_DisposesThirdOfThreeEmpty() + { + var e1 = new DisposalDetectingEnumerable(10, 1); + var e2 = new DisposalDetectingEnumerable(20, 1); + var e3 = new DisposalDetectingEnumerable(0, 0); + var res = e1.Union(e2).Union(e3); + + var e = res.GetAsyncEnumerator(); + await HasNextAsync(e, 10); + await HasNextAsync(e, 20); + await NoNextAsync(e); + + Assert.Single(e1.Enumerators); + Assert.Single(e2.Enumerators); + Assert.Single(e3.Enumerators); + Assert.Equal([true, true, true], [e1.Enumerators[0].Disposed, e2.Enumerators[0].Disposed, e3.Enumerators[0].Disposed]); + } + + [Fact] + public async Task Union_DisposesAllOfTwoEmpty() + { + var e1 = new DisposalDetectingEnumerable(0, 0); + var e2 = new DisposalDetectingEnumerable(0, 0); + var res = e1.Union(e2); + + var e = res.GetAsyncEnumerator(); + await NoNextAsync(e); + + Assert.Single(e1.Enumerators); + Assert.Single(e2.Enumerators); + Assert.Equal([true, true], [e1.Enumerators[0].Disposed, e2.Enumerators[0].Disposed]); + } + + [Fact] + public async Task Union_DisposesAllOfThreeEmpty() + { + var e1 = new DisposalDetectingEnumerable(0, 0); + var e2 = new DisposalDetectingEnumerable(0, 0); + var e3 = new DisposalDetectingEnumerable(0, 0); + var res = e1.Union(e2).Union(e3); + + var e = res.GetAsyncEnumerator(); + await NoNextAsync(e); + + Assert.Single(e1.Enumerators); + Assert.Single(e2.Enumerators); + Assert.Single(e3.Enumerators); + Assert.Equal([true, true, true], [e1.Enumerators[0].Disposed, e2.Enumerators[0].Disposed, e3.Enumerators[0].Disposed]); + } + private sealed class Eq : IEqualityComparer { public bool Equals(int x, int y) @@ -174,5 +297,61 @@ public int GetHashCode(int obj) return EqualityComparer.Default.GetHashCode(Math.Abs(obj)); } } + + private class DisposalDetectingEnumerable : IAsyncEnumerable + { + private readonly int _start; + private readonly int _count; + + public DisposalDetectingEnumerable(int start, int count) + { + _start = start; + _count = count; + } + + public List Enumerators { get; } = new List(); + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + Enumerator r = new(_start, _count); + Enumerators.Add(r); + return r; + } + + public class Enumerator : IAsyncEnumerator + { + private readonly int _max; + + public Enumerator(int start, int count) + { + Current = start - 1; + _max = start + count; + } + + public int Current { get; private set; } + + public bool Disposed { get; private set; } + + public void Dispose() + { + Disposed = true; + } + + public ValueTask DisposeAsync() + { + Disposed = true; + return new ValueTask(); + } + public ValueTask MoveNextAsync() + { + if (++Current < _max) + { + return new ValueTask(true); + } + + return new ValueTask(false); + } + } + } } } diff --git a/Ix.NET/Source/System.Linq.Async/System.Linq.Async.csproj b/Ix.NET/Source/System.Linq.Async/System.Linq.Async.csproj index 5c86d96bff..d09589ba79 100644 --- a/Ix.NET/Source/System.Linq.Async/System.Linq.Async.csproj +++ b/Ix.NET/Source/System.Linq.Async/System.Linq.Async.csproj @@ -27,7 +27,7 @@ - + diff --git a/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Union.cs b/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Union.cs index 126acb895c..c3d26418cc 100644 --- a/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Union.cs +++ b/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Union.cs @@ -127,10 +127,10 @@ protected sealed override async ValueTask MoveNextCore() ++_index; var enumerator = enumerable.GetAsyncEnumerator(_cancellationToken); + await SetEnumeratorAsync(enumerator).ConfigureAwait(false); if (await enumerator.MoveNextAsync().ConfigureAwait(false)) { - await SetEnumeratorAsync(enumerator).ConfigureAwait(false); StoreFirst(); _state = AsyncIteratorState.Iterating; diff --git a/Ix.NET/Source/refs/System.Interactive.Providers.Ref/System.Interactive.Providers.Ref.csproj b/Ix.NET/Source/refs/System.Interactive.Providers.Ref/System.Interactive.Providers.csproj similarity index 88% rename from Ix.NET/Source/refs/System.Interactive.Providers.Ref/System.Interactive.Providers.Ref.csproj rename to Ix.NET/Source/refs/System.Interactive.Providers.Ref/System.Interactive.Providers.csproj index 716b0a8e36..6958af7df7 100644 --- a/Ix.NET/Source/refs/System.Interactive.Providers.Ref/System.Interactive.Providers.Ref.csproj +++ b/Ix.NET/Source/refs/System.Interactive.Providers.Ref/System.Interactive.Providers.csproj @@ -5,11 +5,10 @@ Interactive Extensions - Providers Library net4.8;netstandard2.1;net6.0 Ix;Interactive;Extensions;Enumerable - System.Interactive.Providers - + diff --git a/Ix.NET/Source/refs/System.Interactive.Ref/System.Interactive.Ref.csproj b/Ix.NET/Source/refs/System.Interactive.Ref/System.Interactive.csproj similarity index 91% rename from Ix.NET/Source/refs/System.Interactive.Ref/System.Interactive.Ref.csproj rename to Ix.NET/Source/refs/System.Interactive.Ref/System.Interactive.csproj index 0f8f24d31d..598c60f3b7 100644 --- a/Ix.NET/Source/refs/System.Interactive.Ref/System.Interactive.Ref.csproj +++ b/Ix.NET/Source/refs/System.Interactive.Ref/System.Interactive.csproj @@ -3,7 +3,6 @@ Interactive Extensions Main Library used to express queries over enumerable sequences. Interactive Extensions - Main Library - System.Interactive Microsoft net4.8;netstandard2.1;net6.0 Ix;Interactive;Extensions;Enumerable diff --git a/Ix.NET/Source/refs/System.Linq.Async.Ref/System.Linq.Async.Ref.csproj b/Ix.NET/Source/refs/System.Linq.Async.Ref/System.Linq.Async.csproj similarity index 100% rename from Ix.NET/Source/refs/System.Linq.Async.Ref/System.Linq.Async.Ref.csproj rename to Ix.NET/Source/refs/System.Linq.Async.Ref/System.Linq.Async.csproj diff --git a/azure-pipelines.ix.yml b/azure-pipelines.ix.yml index 758943f21e..5aacc1a459 100644 --- a/azure-pipelines.ix.yml +++ b/azure-pipelines.ix.yml @@ -32,9 +32,15 @@ stages: vmImage: ubuntu-latest steps: - task: UseDotNet@2 - displayName: Use .NET Core 6.x SDK + displayName: Use .NET Core 8.x SDK inputs: - version: 6.x + version: 8.x + + - task: UseDotNet@2 + displayName: .NET 6.0 runtime + inputs: + version: '6.x' + packageType: runtime - task: UseDotNet@2 displayName: .NET Core 3.1 runtime