Browse Source

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.)
Ian Griffiths 1 year ago
parent
commit
4f341fc2d5

+ 3 - 3
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

+ 1 - 1
Ix.NET/Source/System.Interactive.Providers/System.Interactive.Providers.csproj

@@ -13,7 +13,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\System.Interactive\System.Interactive.csproj" />
-    <ReferenceAssemblyProjectReference Include="..\refs\System.Interactive.Providers.Ref\System.Interactive.Providers.Ref.csproj" ReferenceOutputAssembly="false" />
+    <ReferenceAssemblyProjectReference Include="..\refs\System.Interactive.Providers.Ref\System.Interactive.Providers.csproj" ReferenceOutputAssembly="false" />
   </ItemGroup>
 
 </Project>

+ 1 - 1
Ix.NET/Source/System.Interactive/System.Interactive.csproj

@@ -10,7 +10,7 @@
 
   <ItemGroup>
     <EmbeddedResource Include="Properties\System.Interactive.rd.xml" />
-    <ReferenceAssemblyProjectReference Include="..\refs\System.Interactive.Ref\System.Interactive.Ref.csproj" ReferenceOutputAssembly="false" />
+    <ReferenceAssemblyProjectReference Include="..\refs\System.Interactive.Ref\System.Interactive.csproj" ReferenceOutputAssembly="false" />
   </ItemGroup>
   
 </Project>

+ 179 - 0
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 @@ namespace Tests
             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<int>
         {
             public bool Equals(int x, int y)
@@ -174,5 +297,61 @@ namespace Tests
                 return EqualityComparer<int>.Default.GetHashCode(Math.Abs(obj));
             }
         }
+
+        private class DisposalDetectingEnumerable : IAsyncEnumerable<int>
+        {
+            private readonly int _start;
+            private readonly int _count;
+
+            public DisposalDetectingEnumerable(int start, int count)
+            {
+                _start = start;
+                _count = count;
+            }
+
+            public List<Enumerator> Enumerators { get; } = new List<Enumerator>();
+
+            public IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default)
+            {
+                Enumerator r = new(_start, _count);
+                Enumerators.Add(r);
+                return r;
+            }
+
+            public class Enumerator : IAsyncEnumerator<int>
+            {
+                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<bool> MoveNextAsync()
+                {
+                    if (++Current < _max)
+                    {
+                        return new ValueTask<bool>(true);
+                    }
+
+                    return new ValueTask<bool>(false);
+                }
+            }
+        }
     }
 }

+ 1 - 1
Ix.NET/Source/System.Linq.Async/System.Linq.Async.csproj

@@ -27,7 +27,7 @@
 
   <ItemGroup>
     <PackageReference Condition="'$(TargetFramework)' == 'net48' or '$(TargetFramework)' == 'netstandard2.0' " Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
-    <ReferenceAssemblyProjectReference Include="..\refs\System.Linq.Async.Ref\System.Linq.Async.Ref.csproj" />
+    <ReferenceAssemblyProjectReference Include="..\refs\System.Linq.Async.Ref\System.Linq.Async.csproj" />
     <ProjectReference Include="..\System.Linq.Async.SourceGenerator\System.Linq.Async.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" Private="false" />
   </ItemGroup>
 

+ 1 - 1
Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Union.cs

@@ -127,10 +127,10 @@ namespace System.Linq
                             ++_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;

+ 1 - 2
Ix.NET/Source/refs/System.Interactive.Providers.Ref/System.Interactive.Providers.Ref.csproj → Ix.NET/Source/refs/System.Interactive.Providers.Ref/System.Interactive.Providers.csproj

@@ -5,11 +5,10 @@
     <AssemblyTitle>Interactive Extensions - Providers Library</AssemblyTitle>
     <TargetFrameworks>net4.8;netstandard2.1;net6.0</TargetFrameworks>
     <PackageTags>Ix;Interactive;Extensions;Enumerable</PackageTags>
-  <AssemblyName>System.Interactive.Providers</AssemblyName>
   </PropertyGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\System.Interactive.Ref\System.Interactive.Ref.csproj" />
+    <ProjectReference Include="..\System.Interactive.Ref\System.Interactive.csproj" />
   </ItemGroup>
 
   <ItemGroup>

+ 0 - 1
Ix.NET/Source/refs/System.Interactive.Ref/System.Interactive.Ref.csproj → Ix.NET/Source/refs/System.Interactive.Ref/System.Interactive.csproj

@@ -3,7 +3,6 @@
   <PropertyGroup>
     <Description>Interactive Extensions Main Library used to express queries over enumerable sequences.</Description>
     <AssemblyTitle>Interactive Extensions - Main Library</AssemblyTitle>
-    <AssemblyName>System.Interactive</AssemblyName>
     <Authors>Microsoft</Authors>
     <TargetFrameworks>net4.8;netstandard2.1;net6.0</TargetFrameworks>
     <PackageTags>Ix;Interactive;Extensions;Enumerable</PackageTags>

+ 0 - 0
Ix.NET/Source/refs/System.Linq.Async.Ref/System.Linq.Async.Ref.csproj → Ix.NET/Source/refs/System.Linq.Async.Ref/System.Linq.Async.csproj


+ 8 - 2
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