From 92075f2b7e3caa1a75f6c39fa63cc64067172f50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 08:39:06 +0200 Subject: [PATCH 01/43] Bump the all-deps group with 5 updates (#224) Bumps MartinCostello.Logging.XUnit from 0.5.1 to 0.6.0 Bumps Microsoft.Bcl.AsyncInterfaces to 9.0.5, 9.0.5 Bumps Microsoft.Extensions.DependencyInjection to 9.0.5, 9.0.5, 9.0.5 Bumps Microsoft.Extensions.DependencyInjection.Abstractions to 9.0.5, 9.0.5, 9.0.5, 9.0.5 Bumps Microsoft.Extensions.Logging.Abstractions to 9.0.5, 9.0.5 --- updated-dependencies: - dependency-name: MartinCostello.Logging.XUnit dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-deps - dependency-name: Microsoft.Bcl.AsyncInterfaces dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Bcl.AsyncInterfaces dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.Logging.Abstractions dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.Logging.Abstractions dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/CommonUtilities.DownloadManager.csproj | 4 ++-- .../test/CommonUtilities.DownloadManager.Test.csproj | 4 ++-- .../src/CommonUtilities.SimplePipeline.csproj | 4 ++-- .../test/CommonUtilities.SimplePipeline.Test.csproj | 4 ++-- .../CommonUtilities.TestingUtilities.csproj | 4 ++-- src/CommonUtilities/src/CommonUtilities.csproj | 4 ++-- src/CommonUtilities/test/CommonUtilities.Test.csproj | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index da8222c..8ce197b 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -30,8 +30,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 4105013..9716d5b 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -17,12 +17,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index f5ae696..a596a25 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -25,8 +25,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index f8aa949..e16694e 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -21,8 +21,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 3006fe0..5b87015 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -21,8 +21,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index 879548b..40d7c50 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -26,8 +26,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index 9622cea..7704ffb 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -18,8 +18,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 79c0573f1b2ddb3ce0acb585851d38eadb955af0 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 11 Jun 2025 10:43:39 +0200 Subject: [PATCH 02/43] update deps --- .../src/CommonUtilities.DownloadManager.csproj | 4 ++-- .../test/CommonUtilities.DownloadManager.Test.csproj | 6 +++--- .../test/CommonUtilities.FileSystem.Test.csproj | 4 ++-- .../test/CommonUtilities.Registry.Test.csproj | 4 ++-- .../src/CommonUtilities.SimplePipeline.csproj | 4 ++-- .../test/CommonUtilities.SimplePipeline.Test.csproj | 8 ++++---- .../CommonUtilities.TestingUtilities.csproj | 11 ++++++----- src/CommonUtilities/src/CommonUtilities.csproj | 4 ++-- src/CommonUtilities/test/CommonUtilities.Test.csproj | 12 ++++++++---- 9 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index 8ce197b..3f0bb38 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -30,8 +30,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 9716d5b..8097a45 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -22,11 +22,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index 78a5670..7e97959 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -23,14 +23,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj index 771d337..eefcc3c 100644 --- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj +++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj @@ -30,9 +30,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index a596a25..c89ae8f 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -25,8 +25,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index e16694e..9b45d06 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -21,12 +21,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 5b87015..398a9d5 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -13,19 +13,20 @@ + true xUnit2013 - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index 40d7c50..ad6d580 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -26,8 +26,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index 7704ffb..f946662 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -13,17 +13,21 @@ true + + true + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 60037a81d1c24a280439c2ceb2c99cad2313329f Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 11 Jun 2025 10:58:19 +0200 Subject: [PATCH 03/43] fix deps update --- .../test/PathNormalizerTest.Normalize.cs | 4 ++-- .../CommonUtilities.TestingUtilities.csproj | 5 ++--- src/CommonUtilities/test/CommonUtilities.Test.csproj | 8 ++------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs b/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs index 7191087..dbef7e4 100644 --- a/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs +++ b/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs @@ -358,7 +358,7 @@ private static IEnumerable NormalizeTestDataSource() Input = new string('a', 300), ExpectedLinux = new string('a', 300) + "/", ExpectedWindows = new string('a', 300) + "\\", - Options = new PathNormalizeOptions() + Options = new PathNormalizeOptions { TrailingDirectorySeparatorBehavior = TrailingDirectorySeparatorBehavior.Ensure } @@ -368,7 +368,7 @@ private static IEnumerable NormalizeTestDataSource() Input = new string('a', 300) + "/", ExpectedLinux = new string('a', 300), ExpectedWindows = new string('a', 300), - Options = new PathNormalizeOptions() + Options = new PathNormalizeOptions { TrailingDirectorySeparatorBehavior = TrailingDirectorySeparatorBehavior.Trim } diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 398a9d5..4b3cd48 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -1,7 +1,7 @@ - + - net9.0;net6.0 + net9.0 $(TargetFrameworks);net48 enable enable @@ -13,7 +13,6 @@ - true xUnit2013 diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index f946662..cc7084d 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -1,7 +1,7 @@ - + - net9.0;net6.0 + net9.0;net8.0 $(TargetFrameworks);net48 false true @@ -13,10 +13,6 @@ true - - true - - all From 327ed61edc916c6f15366a3a9194a5eeb28e4a34 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Wed, 11 Jun 2025 11:01:11 +0200 Subject: [PATCH 04/43] try fix --- .../CommonUtilities.TestingUtilities.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 4b3cd48..916bbea 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -1,7 +1,7 @@  - net9.0 + net9.0;net8.0 $(TargetFrameworks);net48 enable enable From 41426284e95e1227b2536476540a3b579b1d3e42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:27:49 +0200 Subject: [PATCH 05/43] Bump the all-deps group with 1 update (#227) Bumps System.Linq.Async to 6.0.3 --- updated-dependencies: - dependency-name: System.Linq.Async dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: System.Linq.Async dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../test/CommonUtilities.SimplePipeline.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index 9b45d06..ddcfb31 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -24,7 +24,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive From f554717d285de43fcea25f1d653893051910140a Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Tue, 15 Jul 2025 13:07:51 +0200 Subject: [PATCH 06/43] update deps & fix test --- .../src/CommonUtilities.DownloadManager.csproj | 4 ++-- .../test/CommonUtilities.DownloadManager.Test.csproj | 6 +++--- .../test/DownloadManagerTest.cs | 2 +- .../src/Commonutilities.FileSystem.csproj | 2 +- .../test/CommonUtilities.FileSystem.Test.csproj | 4 ++-- .../test/CommonUtilities.Registry.Test.csproj | 2 +- .../src/CommonUtilities.SimplePipeline.csproj | 4 ++-- .../test/CommonUtilities.SimplePipeline.Test.csproj | 6 +++--- .../CommonUtilities.TestingUtilities.csproj | 8 ++++---- src/CommonUtilities/src/CommonUtilities.csproj | 4 ++-- src/CommonUtilities/test/CommonUtilities.Test.csproj | 8 ++++---- 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index 3f0bb38..b5c0146 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -30,8 +30,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 8097a45..f5b3e1c 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -22,11 +22,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs index be76d26..42b1c9b 100644 --- a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs +++ b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs @@ -216,7 +216,7 @@ public async Task DownloadAsync_GithubApi() await DownloadAsyncTest(provider, uri, true, new DownloadOptions { UserAgent = "AnakinRaw.DownloadManager.Test" }, null); - Assert.Equal(2566, FileSystem.File.ReadAllText(Destination).Length); + Assert.Equal(2584, FileSystem.File.ReadAllText(Destination).Length); } #if NETFRAMEWORK diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj index e2e2045..61e0960 100644 --- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj +++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj @@ -33,7 +33,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index 7e97959..2353e51 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -28,9 +28,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj index eefcc3c..f25c657 100644 --- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj +++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj @@ -32,7 +32,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index c89ae8f..70a362d 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -25,8 +25,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index ddcfb31..fc5062a 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -21,12 +21,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 916bbea..9b967fe 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -21,16 +21,16 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index ad6d580..0659a0b 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -26,8 +26,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index cc7084d..1f0d922 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -18,12 +18,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all From c5b1b1dbfabd1f535c62eb165fc414ea5a066f5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:37:26 +0200 Subject: [PATCH 07/43] Bump Microsoft.Bcl.AsyncInterfaces from 6.0.0 to 9.0.7 (#238) --- updated-dependencies: - dependency-name: Microsoft.Bcl.AsyncInterfaces dependency-version: 9.0.7 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../CommonUtilities.TestingUtilities.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 9b967fe..13da3c3 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -21,6 +21,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + From d0b30e939957ce300beb79e9e3a3f1fcd0428dc2 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 16 Aug 2025 19:16:21 +0200 Subject: [PATCH 08/43] update deps --- .../src/CommonUtilities.DownloadManager.csproj | 4 ++-- .../test/CommonUtilities.DownloadManager.Test.csproj | 6 +++--- .../src/Commonutilities.FileSystem.csproj | 2 +- .../test/CommonUtilities.FileSystem.Test.csproj | 4 ++-- .../test/CommonUtilities.Registry.Test.csproj | 2 +- .../src/CommonUtilities.SimplePipeline.csproj | 4 ++-- .../test/CommonUtilities.SimplePipeline.Test.csproj | 6 +++--- .../CommonUtilities.TestingUtilities.csproj | 10 +++++----- src/CommonUtilities/src/CommonUtilities.csproj | 4 ++-- src/CommonUtilities/test/CommonUtilities.Test.csproj | 8 ++++---- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index b5c0146..584403b 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -30,8 +30,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index f5b3e1c..63119e4 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -22,11 +22,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj index 61e0960..5b6fd5f 100644 --- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj +++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj @@ -33,7 +33,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index 2353e51..613f4a4 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -28,9 +28,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj index f25c657..a9afdce 100644 --- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj +++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj @@ -32,7 +32,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index 70a362d..319f4ea 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -25,8 +25,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index fc5062a..b6bb95a 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -21,12 +21,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 13da3c3..30f9a8b 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -21,17 +21,17 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index 0659a0b..0b027be 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -26,8 +26,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index 1f0d922..bbdf7f5 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -18,12 +18,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 1725ecaf35bb23cd05b46c634a556eeb3232ba60 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sat, 16 Aug 2025 19:25:20 +0200 Subject: [PATCH 09/43] fix test --- src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs index 42b1c9b..8f185d9 100644 --- a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs +++ b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs @@ -216,7 +216,7 @@ public async Task DownloadAsync_GithubApi() await DownloadAsyncTest(provider, uri, true, new DownloadOptions { UserAgent = "AnakinRaw.DownloadManager.Test" }, null); - Assert.Equal(2584, FileSystem.File.ReadAllText(Destination).Length); + Assert.Equal(2620, FileSystem.File.ReadAllText(Destination).Length); } #if NETFRAMEWORK From 88c27765bd46842acf0391e0859434207a708922 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:32:00 +0200 Subject: [PATCH 10/43] Bump the actions-deps group across 1 directory with 2 updates (#245) Bumps the actions-deps group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/checkout` from 4 to 5 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) Updates `actions/download-artifact` from 4 to 5 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/download-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: AnakinRaW --- .github/workflows/release.yml | 6 +++--- .github/workflows/test.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 878b324..83afd69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup .NET @@ -40,12 +40,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup .NET uses: actions/setup-dotnet@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: NuGet packages path: packages diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d9bcbd8..42c6320 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: actions/setup-dotnet@v4 From 2b7c72270e2d72d4e3971db865c09c3cb3faa47e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:13:19 +0200 Subject: [PATCH 11/43] Bump actions/setup-dotnet from 4 to 5 in the actions-deps group (#247) Bumps the actions-deps group with 1 update: [actions/setup-dotnet](https://github.com/actions/setup-dotnet). Updates `actions/setup-dotnet` from 4 to 5 - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83afd69..90c181b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: with: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '9.0.x' - name: Create packages @@ -44,7 +44,7 @@ jobs: with: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 - uses: actions/download-artifact@v5 with: name: NuGet packages diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42c6320..4aa87c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: dotnet-version: | 6.0.x From 5f3efd821c6213b88a5dd287890d49e33ff0994f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 09:24:30 +0200 Subject: [PATCH 12/43] Bump the all-deps group with 6 updates (#253) Bumps Microsoft.Bcl.AsyncInterfaces from 9.0.8 to 9.0.9 Bumps Microsoft.Extensions.DependencyInjection from 9.0.8 to 9.0.9 Bumps Microsoft.Extensions.DependencyInjection.Abstractions from 9.0.8 to 9.0.9 Bumps Microsoft.Extensions.Logging.Abstractions from 9.0.8 to 9.0.9 Bumps Testably.Abstractions.AccessControl from 4.3.2 to 4.3.6 Bumps Testably.Abstractions.Testing from 4.3.2 to 4.3.6 --- updated-dependencies: - dependency-name: Microsoft.Bcl.AsyncInterfaces dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Bcl.AsyncInterfaces dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.Logging.Abstractions dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.Logging.Abstractions dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Microsoft.Extensions.Logging.Abstractions dependency-version: 9.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Testably.Abstractions.AccessControl dependency-version: 4.3.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Testably.Abstractions.Testing dependency-version: 4.3.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Testably.Abstractions.Testing dependency-version: 4.3.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Testably.Abstractions.Testing dependency-version: 4.3.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps - dependency-name: Testably.Abstractions.Testing dependency-version: 4.3.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/CommonUtilities.DownloadManager.csproj | 4 ++-- .../test/CommonUtilities.DownloadManager.Test.csproj | 4 ++-- .../src/Commonutilities.FileSystem.csproj | 2 +- .../test/CommonUtilities.FileSystem.Test.csproj | 2 +- .../src/CommonUtilities.SimplePipeline.csproj | 4 ++-- .../test/CommonUtilities.SimplePipeline.Test.csproj | 4 ++-- .../CommonUtilities.TestingUtilities.csproj | 8 ++++---- src/CommonUtilities/src/CommonUtilities.csproj | 4 ++-- src/CommonUtilities/test/CommonUtilities.Test.csproj | 6 +++--- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index 584403b..7c96736 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -30,8 +30,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 63119e4..91aa092 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -22,9 +22,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj index 5b6fd5f..8cff20b 100644 --- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj +++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj @@ -33,7 +33,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index 613f4a4..e7304d5 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -28,7 +28,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index 319f4ea..05e8aaa 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -25,8 +25,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index b6bb95a..be36538 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -21,8 +21,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 30f9a8b..4808229 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -21,9 +21,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + @@ -31,7 +31,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index 0b027be..149a376 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -26,8 +26,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index bbdf7f5..eae50ec 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -18,10 +18,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive From 662f9d98fbcf87674f4dcec8b6d209ed0875e1f2 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Thu, 2 Oct 2025 09:35:35 +0200 Subject: [PATCH 13/43] update deps --- Directory.Build.props | 2 +- .../test/CommonUtilities.DownloadManager.Test.csproj | 4 ++-- .../src/Commonutilities.FileSystem.csproj | 2 +- .../test/CommonUtilities.FileSystem.Test.csproj | 4 ++-- .../test/CommonUtilities.Registry.Test.csproj | 2 +- .../test/CommonUtilities.SimplePipeline.Test.csproj | 2 +- .../CommonUtilities.TestingUtilities.csproj | 4 ++-- src/CommonUtilities/test/CommonUtilities.Test.csproj | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index f7a8bf7..54a1c15 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -25,7 +25,7 @@ all - 3.7.115 + 3.8.118 diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 91aa092..d2f003d 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -24,9 +24,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj index 8cff20b..93a8dce 100644 --- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj +++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj @@ -33,7 +33,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index e7304d5..cc478bd 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -28,9 +28,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj index a9afdce..7a22740 100644 --- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj +++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj @@ -32,7 +32,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index be36538..ae6631d 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -26,7 +26,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 4808229..1226115 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -26,12 +26,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index eae50ec..d425c2c 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -21,9 +21,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 9175dd38e9da60a4346c288424799d84bfb8ffdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:11:26 +0200 Subject: [PATCH 14/43] Bump the all-deps group with 1 update (#264) Bumps Microsoft.NET.Test.Sdk from 17.14.1 to 18.0.0 --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-deps - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-deps - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-deps - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-deps - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-deps - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../test/CommonUtilities.DownloadManager.Test.csproj | 2 +- .../test/CommonUtilities.FileSystem.Test.csproj | 2 +- .../test/CommonUtilities.Registry.Test.csproj | 2 +- .../test/CommonUtilities.SimplePipeline.Test.csproj | 2 +- .../CommonUtilities.TestingUtilities.csproj | 2 +- src/CommonUtilities/test/CommonUtilities.Test.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index d2f003d..80f5cac 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -23,7 +23,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index cc478bd..330e8e0 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj index 7a22740..9452aff 100644 --- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj +++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj @@ -30,7 +30,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index ae6631d..c387af0 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 1226115..01b2507 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -24,7 +24,7 @@ - + all diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index d425c2c..3433909 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -20,7 +20,7 @@ - + From eed19a7a784be0532b2199bf883099745e74c3ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:03:55 +0100 Subject: [PATCH 15/43] Bump the actions-deps group across 1 directory with 3 updates (#286) Bumps the actions-deps group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/checkout` from 5 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) Updates `actions/upload-artifact` from 4 to 5 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) Updates `actions/download-artifact` from 5 to 6 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 8 ++++---- .github/workflows/test.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90c181b..142b2d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup .NET @@ -27,7 +27,7 @@ jobs: - name: Create packages run: dotnet pack --configuration Release --output ./packages - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: NuGet packages path: packages/*.* @@ -40,12 +40,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup .NET uses: actions/setup-dotnet@v5 - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: NuGet packages path: packages diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4aa87c2..f3d3195 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-dotnet@v5 From 5ef2eac27301d357929a36b6977988b836a93fb8 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 7 Dec 2025 17:06:10 +0100 Subject: [PATCH 16/43] migrate solution --- CommonUtilities.sln | 107 ------------------------------------------- CommonUtilities.slnx | 23 ++++++++++ 2 files changed, 23 insertions(+), 107 deletions(-) delete mode 100644 CommonUtilities.sln create mode 100644 CommonUtilities.slnx diff --git a/CommonUtilities.sln b/CommonUtilities.sln deleted file mode 100644 index 7b71a5c..0000000 --- a/CommonUtilities.sln +++ /dev/null @@ -1,107 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31717.71 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommonUtilities", "CommonUtilities", "{9CD34E8E-2082-46F9-B7EE-3DD4D1385B04}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities", "src\CommonUtilities\src\CommonUtilities.csproj", "{59CB2F45-CC0C-46C3-B6D6-581A7C123295}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.Test", "src\CommonUtilities\test\CommonUtilities.Test.csproj", "{EAEF69D0-BA59-4397-ABC0-5E7ED397FDB4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommonUtilities.FileSystem", "CommonUtilities.FileSystem", "{6DC3D9F6-3394-46E3-B582-6A022FCCE397}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Commonutilities.FileSystem", "src\CommonUtilities.FileSystem\src\Commonutilities.FileSystem.csproj", "{629247FB-648B-4F7D-9039-15D74102B4C5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.FileSystem.Test", "src\CommonUtilities.FileSystem\test\CommonUtilities.FileSystem.Test.csproj", "{C6AB748E-AEC2-4793-A8D6-F91F46CE098C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommonUtilities.Registry", "CommonUtilities.Registry", "{EDF446DD-BE04-4FE2-93A5-6171A0EC8C82}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.Registry", "src\CommonUtilities.Registry\src\CommonUtilities.Registry.csproj", "{2BE46A1C-8D01-422D-8FD5-A2885305A92D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.Registry.Test", "src\CommonUtilities.Registry\test\CommonUtilities.Registry.Test.csproj", "{375178FD-0D91-43B7-A731-1419808718E5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommonUtilities.DownloadManager", "CommonUtilities.DownloadManager", "{F306B877-4E2C-4D6F-BF68-96D2BD23FB69}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.DownloadManager", "src\CommonUtilities.DownloadManager\src\CommonUtilities.DownloadManager.csproj", "{9F9E1DA3-E4DC-4590-A3BF-09AA9286271E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.DownloadManager.Test", "src\CommonUtilities.DownloadManager\test\CommonUtilities.DownloadManager.Test.csproj", "{B67F61B6-489C-4036-9380-8B8DB45AB295}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommonUtilities.SimplePipeline", "CommonUtilities.SimplePipeline", "{9D9B958B-7495-4614-8D0D-47587C8FEBC4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.SimplePipeline", "src\CommonUtilities.SimplePipeline\src\CommonUtilities.SimplePipeline.csproj", "{283BBAA8-482D-4DB5-9707-3F325D2AE41F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.SimplePipeline.Test", "src\CommonUtilities.SimplePipeline\test\CommonUtilities.SimplePipeline.Test.csproj", "{C8BF3F01-B1D5-4C29-9164-6DC7B9744589}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUtilities.TestingUtilities", "src\CommonUtilities.TestingUtilities\CommonUtilities.TestingUtilities.csproj", "{99A3B9B4-6482-410A-A001-9D62F4B259CC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {59CB2F45-CC0C-46C3-B6D6-581A7C123295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {59CB2F45-CC0C-46C3-B6D6-581A7C123295}.Debug|Any CPU.Build.0 = Debug|Any CPU - {59CB2F45-CC0C-46C3-B6D6-581A7C123295}.Release|Any CPU.ActiveCfg = Release|Any CPU - {59CB2F45-CC0C-46C3-B6D6-581A7C123295}.Release|Any CPU.Build.0 = Release|Any CPU - {EAEF69D0-BA59-4397-ABC0-5E7ED397FDB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EAEF69D0-BA59-4397-ABC0-5E7ED397FDB4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EAEF69D0-BA59-4397-ABC0-5E7ED397FDB4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EAEF69D0-BA59-4397-ABC0-5E7ED397FDB4}.Release|Any CPU.Build.0 = Release|Any CPU - {629247FB-648B-4F7D-9039-15D74102B4C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {629247FB-648B-4F7D-9039-15D74102B4C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {629247FB-648B-4F7D-9039-15D74102B4C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {629247FB-648B-4F7D-9039-15D74102B4C5}.Release|Any CPU.Build.0 = Release|Any CPU - {C6AB748E-AEC2-4793-A8D6-F91F46CE098C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C6AB748E-AEC2-4793-A8D6-F91F46CE098C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C6AB748E-AEC2-4793-A8D6-F91F46CE098C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C6AB748E-AEC2-4793-A8D6-F91F46CE098C}.Release|Any CPU.Build.0 = Release|Any CPU - {2BE46A1C-8D01-422D-8FD5-A2885305A92D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2BE46A1C-8D01-422D-8FD5-A2885305A92D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BE46A1C-8D01-422D-8FD5-A2885305A92D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2BE46A1C-8D01-422D-8FD5-A2885305A92D}.Release|Any CPU.Build.0 = Release|Any CPU - {375178FD-0D91-43B7-A731-1419808718E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {375178FD-0D91-43B7-A731-1419808718E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {375178FD-0D91-43B7-A731-1419808718E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {375178FD-0D91-43B7-A731-1419808718E5}.Release|Any CPU.Build.0 = Release|Any CPU - {9F9E1DA3-E4DC-4590-A3BF-09AA9286271E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F9E1DA3-E4DC-4590-A3BF-09AA9286271E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F9E1DA3-E4DC-4590-A3BF-09AA9286271E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F9E1DA3-E4DC-4590-A3BF-09AA9286271E}.Release|Any CPU.Build.0 = Release|Any CPU - {B67F61B6-489C-4036-9380-8B8DB45AB295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B67F61B6-489C-4036-9380-8B8DB45AB295}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B67F61B6-489C-4036-9380-8B8DB45AB295}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B67F61B6-489C-4036-9380-8B8DB45AB295}.Release|Any CPU.Build.0 = Release|Any CPU - {283BBAA8-482D-4DB5-9707-3F325D2AE41F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {283BBAA8-482D-4DB5-9707-3F325D2AE41F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {283BBAA8-482D-4DB5-9707-3F325D2AE41F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {283BBAA8-482D-4DB5-9707-3F325D2AE41F}.Release|Any CPU.Build.0 = Release|Any CPU - {C8BF3F01-B1D5-4C29-9164-6DC7B9744589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C8BF3F01-B1D5-4C29-9164-6DC7B9744589}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C8BF3F01-B1D5-4C29-9164-6DC7B9744589}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C8BF3F01-B1D5-4C29-9164-6DC7B9744589}.Release|Any CPU.Build.0 = Release|Any CPU - {99A3B9B4-6482-410A-A001-9D62F4B259CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99A3B9B4-6482-410A-A001-9D62F4B259CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99A3B9B4-6482-410A-A001-9D62F4B259CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99A3B9B4-6482-410A-A001-9D62F4B259CC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {59CB2F45-CC0C-46C3-B6D6-581A7C123295} = {9CD34E8E-2082-46F9-B7EE-3DD4D1385B04} - {EAEF69D0-BA59-4397-ABC0-5E7ED397FDB4} = {9CD34E8E-2082-46F9-B7EE-3DD4D1385B04} - {629247FB-648B-4F7D-9039-15D74102B4C5} = {6DC3D9F6-3394-46E3-B582-6A022FCCE397} - {C6AB748E-AEC2-4793-A8D6-F91F46CE098C} = {6DC3D9F6-3394-46E3-B582-6A022FCCE397} - {2BE46A1C-8D01-422D-8FD5-A2885305A92D} = {EDF446DD-BE04-4FE2-93A5-6171A0EC8C82} - {375178FD-0D91-43B7-A731-1419808718E5} = {EDF446DD-BE04-4FE2-93A5-6171A0EC8C82} - {9F9E1DA3-E4DC-4590-A3BF-09AA9286271E} = {F306B877-4E2C-4D6F-BF68-96D2BD23FB69} - {B67F61B6-489C-4036-9380-8B8DB45AB295} = {F306B877-4E2C-4D6F-BF68-96D2BD23FB69} - {283BBAA8-482D-4DB5-9707-3F325D2AE41F} = {9D9B958B-7495-4614-8D0D-47587C8FEBC4} - {C8BF3F01-B1D5-4C29-9164-6DC7B9744589} = {9D9B958B-7495-4614-8D0D-47587C8FEBC4} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FAF1C2A9-02FC-459D-BE1E-BDB46E72F077} - EndGlobalSection -EndGlobal diff --git a/CommonUtilities.slnx b/CommonUtilities.slnx new file mode 100644 index 0000000..da8c736 --- /dev/null +++ b/CommonUtilities.slnx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + From 093d3c7d4611b35c26d4d5a104e26f7f04503e99 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 7 Dec 2025 17:18:36 +0100 Subject: [PATCH 17/43] update .net --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 1 + Directory.Build.props | 2 +- .../src/CommonUtilities.DownloadManager.csproj | 4 ++-- .../CommonUtilities.DownloadManager.Test.csproj | 14 +++++++------- .../src/Commonutilities.FileSystem.csproj | 4 ++-- .../test/CommonUtilities.FileSystem.Test.csproj | 10 +++++----- .../test/CommonUtilities.Registry.Test.csproj | 8 ++++---- .../src/CommonUtilities.SimplePipeline.csproj | 4 ++-- .../CommonUtilities.SimplePipeline.Test.csproj | 14 +++++++------- .../CommonUtilities.TestingUtilities.csproj | 16 ++++++++-------- src/CommonUtilities/src/CommonUtilities.csproj | 6 +++--- .../test/CommonUtilities.Test.csproj | 14 +++++++------- 13 files changed, 50 insertions(+), 49 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 142b2d4..f807c2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Create packages run: dotnet pack --configuration Release --output ./packages - name: Upload a Build Artifact diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3d3195..d6b9298 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Build & Test in Release Mode run: dotnet test --configuration Release --logger "GitHubActions" \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 54a1c15..9fa329e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -25,7 +25,7 @@ all - 3.8.118 + 3.9.50 diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index 7c96736..b3c74e5 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -30,8 +30,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 80f5cac..4ead415 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -1,8 +1,8 @@ - net9.0;net8.0 - $(TargetFrameworks);net48 + net10.0;net8.0 + $(TargetFrameworks);net481 false true @@ -17,14 +17,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj index 93a8dce..06387b7 100644 --- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj +++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj @@ -26,14 +26,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index 330e8e0..f98eaed 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -1,8 +1,8 @@ - net9.0;net8.0 - $(TargetFrameworks);net48 + net10.0;net8.0 + $(TargetFrameworks);net481 false true enable @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,12 +23,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj index 9452aff..cb77051 100644 --- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj +++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj @@ -1,8 +1,8 @@  - net9.0 - $(TargetFrameworks);net48 + net10.0 + $(TargetFrameworks);net481 false true enable @@ -26,11 +26,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index 05e8aaa..318ea7f 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -25,8 +25,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index c387af0..fd1fc46 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -1,8 +1,8 @@ - net9.0;net8.0 - $(TargetFrameworks);net48 + net10.0;net8.0 + $(TargetFrameworks);net481 false true @@ -17,14 +17,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 01b2507..f80f3b1 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -1,8 +1,8 @@  - net9.0;net8.0 - $(TargetFrameworks);net48 + net10.0;net8.0 + $(TargetFrameworks);net481 enable enable false @@ -21,17 +21,17 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index 149a376..b2951cd 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -26,9 +26,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index 3433909..3948201 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -1,8 +1,8 @@  - net9.0;net8.0 - $(TargetFrameworks);net48 + net10.0;net8.0 + $(TargetFrameworks);net481 false true @@ -14,14 +14,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive From 83c15cf34b99340f717bec2da67e5f51a135a57f Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 7 Dec 2025 18:20:55 +0100 Subject: [PATCH 18/43] reduce some warnings --- .../src/Validation/HashDownloadValidator.cs | 1 - .../test/DownloadManagerTest.cs | 12 +-- .../test/Providers/HttpClientDownloadTest.cs | 1 + .../Validation/HashDownloadValidatorTest.cs | 12 ++- .../Extensions/PathExtensions.NetFramework.cs | 5 +- .../src/Utilities/ValueStringBuilder.cs | 4 +- .../Windows/WindowsFileSystemExtensions.cs | 2 +- .../PathExtensionsTest.GetDirectoryName.cs | 1 + .../test/PathExtensionsTest.GetExtension.cs | 13 +-- .../test/PathExtensionsTest.GetFileName.cs | 1 + .../test/PathExtensionsTest.GetPathRoot.cs | 51 +++++----- .../PathExtensionsTest.GetRelativePathEx.cs | 72 +++++++------- ...hExtensionsTest.HasLeadingPathSeparator.cs | 21 ++-- ...ExtensionsTest.HasTrailingPathSeparator.cs | 21 ++-- ...PathExtensionsTest.IsPathFullyQualified.cs | 14 +-- .../test/PathExtensionsTest.Join.cs | 1 + .../test/Utilities/ValueStringBuilderTest.cs | 98 +++++++++---------- .../src/IRegistryKey.cs | 3 +- .../src/InMemoryRegistryKeyData.cs | 2 +- .../Extensions/InMemoryKeyExtensionsTest.cs | 1 + .../RegistryKeyExtensionsTestBase.cs | 2 +- .../Extensions/WindowsKeyExtensionsTest.cs | 1 + .../test/RegistryKey_GetValueOrDefault.cs | 2 +- .../test/RegistryKey_GetValueOrSetDefault.cs | 2 +- .../test/RegistryTestsBase.cs | 50 +++++----- .../src/Runners/StepRunnerBase.cs | 2 +- .../src/Steps/RunPipelineStep.cs | 1 + .../test/Pipelines/ParallelPipelineTests.cs | 1 + .../ParallelProducerConsumerPipelineTest.cs | 4 + .../AggregatedProgressReporterTests.cs | 3 + .../test/Steps/WaitStepTest.cs | 2 +- .../test/TestStep.cs | 2 +- .../Collections/CollectionsTestSuite.cs | 4 +- .../Collections/ICollectionTestSuite.cs | 2 +- .../INonModifyingEnumerableTestSuite.cs | 2 + .../src/Collections/FrugalList.cs | 10 +- .../src/Collections/ReadOnlyFrugalList.cs | 11 ++- .../src/Hashing/HashTypeKey.cs | 1 + .../src/Hashing/Providers/MD5HashProvider.cs | 1 + .../src/Hashing/Providers/SHA1HashProvider.cs | 1 + .../Hashing/Providers/SHA256HashProvider.cs | 1 + .../Hashing/Providers/SHA384HashProvider.cs | 1 + .../Hashing/Providers/SHA512HashProvider.cs | 1 + .../src/NativeMethods/AdvApi32.cs | 1 + .../test/Collections/FrugalListTest.cs | 1 + .../Collections/ReadOnlyFrugalListTests.cs | 1 + .../test/Extensions/EncodingExtensionsTest.cs | 6 +- .../test/Hashing/HashTypeKeyTest.cs | 2 +- .../test/Hashing/HashingServiceTest.cs | 4 +- 49 files changed, 248 insertions(+), 210 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs index 33364d8..636952e 100644 --- a/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs +++ b/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using AnakinRaW.CommonUtilities.Hashing; diff --git a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs index 8f185d9..58cb97b 100644 --- a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs +++ b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs @@ -405,12 +405,12 @@ public async Task DownloadAsync_CancellationDoesNotRetry_Throws() { counter++; cts.Cancel(); - }, ServiceProvider); + }); var provider2 = new DelegatingFileDownloadProvider("B", () => { counter++; cts.Cancel(); - }, ServiceProvider); + }); manager.AddDownloadProvider(provider1); manager.AddDownloadProvider(provider2); @@ -470,7 +470,7 @@ public Task Validate(Stream stream, long downloadedBytes, CancellationToke } } - private class DelegatingFileDownloadProvider(string name, Action onDownload, IServiceProvider serviceProvider) : IDownloadProvider + private class DelegatingFileDownloadProvider(string name, Action onDownload) : IDownloadProvider { public string Name => name; @@ -549,10 +549,10 @@ public override void Write(byte[] buffer, int offset, int count) throw new NotImplementedException(); } - public override bool CanRead { get; } - public override bool CanSeek { get; } + public override bool CanRead => false; + public override bool CanSeek => false; public override bool CanWrite => false; - public override long Length { get; } + public override long Length => 0; public override long Position { get; set; } } } \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/test/Providers/HttpClientDownloadTest.cs b/src/CommonUtilities.DownloadManager/test/Providers/HttpClientDownloadTest.cs index 84b4844..184248a 100644 --- a/src/CommonUtilities.DownloadManager/test/Providers/HttpClientDownloadTest.cs +++ b/src/CommonUtilities.DownloadManager/test/Providers/HttpClientDownloadTest.cs @@ -5,6 +5,7 @@ namespace AnakinRaW.CommonUtilities.DownloadManager.Test.Providers; +// ReSharper disable once UnusedMember.Global public class HttpClientDownloadTest : InternetDownloadTest { protected override Type ExpectedSourceNotFoundExceptionType => typeof(HttpRequestException); diff --git a/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs b/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs index 10d3ac4..1b83269 100644 --- a/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs +++ b/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Threading.Tasks; using AnakinRaW.CommonUtilities.DownloadManager.Validation; @@ -8,6 +7,9 @@ using AnakinRaW.CommonUtilities.Testing; using Microsoft.Extensions.DependencyInjection; using Xunit; +#if !NET +using System.Globalization; +#endif namespace AnakinRaW.CommonUtilities.DownloadManager.Test.Validation; @@ -188,10 +190,10 @@ public override void Write(byte[] buffer, int offset, int count) throw new NotImplementedException(); } - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite { get; } - public override long Length { get; } + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => 0; public override long Position { get; set; } } } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs index 9608d9c..28bac17 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs @@ -3,6 +3,7 @@ using System.IO.Abstractions; using System; using System.IO; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.FileSystem; @@ -22,7 +23,7 @@ public static partial class PathExtensions /// /// The path to search for an extension. /// if the characters that follow the last directory separator character or volume separator - /// in the path include a period (".") followed by one or more characters; otherwise, . + /// in the path include a period ('.') followed by one or more characters; otherwise, . public static bool HasExtension(this IPath _, ReadOnlySpan path) { for (var i = path.Length - 1; i >= 0; i--) @@ -347,7 +348,7 @@ private static bool IsDeviceUNC(ReadOnlySpan path) private static bool IsExtended(ReadOnlySpan path) { // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths. - // Skipping of normalization will *only* occur if back slashes ('\') are used. + // Skipping of normalization will *only* occur if backslashes ('\') are used. return path.Length >= DevicePrefixLength && path[0] == '\\' && (path[1] == '\\' || path[1] == '?') diff --git a/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs b/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs index 1e4e1ce..88952b5 100644 --- a/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs +++ b/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs @@ -270,13 +270,13 @@ private void Grow(int additionalCapacityBeyondPos) Debug.Assert(additionalCapacityBeyondPos > 0); Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); - const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + const uint arrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try // to double the size if possible, bounding the doubling to not go beyond the max array length. var newCapacity = (int)Math.Max( (uint)(_pos + additionalCapacityBeyondPos), - Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); + Math.Min((uint)_chars.Length * 2, arrayMaxLength)); // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. // This could also go negative if the actual required length wraps around. diff --git a/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs b/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs index 603635e..15a3447 100644 --- a/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs @@ -68,7 +68,7 @@ private static bool AddPendingFileRename(string source, string? destination) try { using var registryKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default); - using var subKey = registryKey.CreateSubKey(sessionManagerKeyPath)!; + using var subKey = registryKey.CreateSubKey(sessionManagerKeyPath); var stringBuilder = new StringBuilder("\\??\\" + source + "\0"); if (!string.IsNullOrEmpty(destination)) stringBuilder.Append("\\??\\" + destination); diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetDirectoryName.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetDirectoryName.cs index 8bf8058..aecfd0d 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetDirectoryName.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetDirectoryName.cs @@ -3,6 +3,7 @@ using System.IO.Abstractions; using Testably.Abstractions; using Xunit; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.FileSystem.Test; diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetExtension.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetExtension.cs index f6011ec..0510d35 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetExtension.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetExtension.cs @@ -2,6 +2,7 @@ using System.IO.Abstractions; using Testably.Abstractions; using Xunit; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.FileSystem.Test; @@ -11,12 +12,12 @@ public class GetExtensionTest public static TheoryData TestData_GetExtension => new() { - { @"file.exe", ".exe" }, - { @"file", "" }, - { @"file.", "" }, - { @"file.s", ".s" }, - { @"test/file", "" }, - { @"test/file.extension", ".extension" }, + { "file.exe", ".exe" }, + { "file", "" }, + { "file.", "" }, + { "file.s", ".s" }, + { "test/file", "" }, + { "test/file.extension", ".extension" }, { @"test\file", "" }, { @"test\file.extension", ".extension" }, { "file.e xe", ".e xe"}, diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs index c591fd3..0b3f321 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs @@ -5,6 +5,7 @@ using AnakinRaW.CommonUtilities.Testing; using Testably.Abstractions; using Xunit; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.FileSystem.Test; diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs index 370a5fc..464f5d8 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs @@ -4,6 +4,7 @@ using AnakinRaW.CommonUtilities.Testing; using Testably.Abstractions; using Xunit; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.FileSystem.Test; @@ -28,44 +29,44 @@ public void GetPathRoot_Basic() Assert.True(_fileSystem.Path.IsPathRooted(cwd)); - Assert.Equal(string.Empty, _fileSystem.Path.GetPathRoot(@"file.exe")); - Assert.True(_fileSystem.Path.GetPathRoot(@"file.exe".AsSpan()).IsEmpty); + Assert.Equal(string.Empty, _fileSystem.Path.GetPathRoot("file.exe")); + Assert.True(_fileSystem.Path.GetPathRoot("file.exe".AsSpan()).IsEmpty); Assert.False(_fileSystem.Path.IsPathRooted("file.exe")); } [PlatformSpecificTheory(TestPlatformIdentifier.Linux)] - [InlineData(@"/../../.././tmp/..")] - [InlineData(@"/../../../")] - [InlineData(@"/../../../tmp/bar/..")] - [InlineData(@"/../.././././bar/../../../")] - [InlineData(@"/../../././tmp/..")] - [InlineData(@"/../../tmp/../../")] - [InlineData(@"/../../tmp/bar/..")] - [InlineData(@"/../tmp/../..")] - [InlineData(@"/././../../../../")] - [InlineData(@"/././../../../")] - [InlineData(@"/./././bar/../../../")] - [InlineData(@"/")] - [InlineData(@"/bar")] - [InlineData(@"/bar/././././../../..")] - [InlineData(@"/bar/tmp")] - [InlineData(@"/tmp/..")] - [InlineData(@"/tmp/../../../../../bar")] - [InlineData(@"/tmp/../../../bar")] - [InlineData(@"/tmp/../bar/../..")] - [InlineData(@"/tmp/bar")] - [InlineData(@"/tmp/bar/..")] + [InlineData("/../../.././tmp/..")] + [InlineData("/../../../")] + [InlineData("/../../../tmp/bar/..")] + [InlineData("/../.././././bar/../../../")] + [InlineData("/../../././tmp/..")] + [InlineData("/../../tmp/../../")] + [InlineData("/../../tmp/bar/..")] + [InlineData("/../tmp/../..")] + [InlineData("/././../../../../")] + [InlineData("/././../../../")] + [InlineData("/./././bar/../../../")] + [InlineData("/")] + [InlineData("/bar")] + [InlineData("/bar/././././../../..")] + [InlineData("/bar/tmp")] + [InlineData("/tmp/..")] + [InlineData("/tmp/../../../../../bar")] + [InlineData("/tmp/../../../bar")] + [InlineData("/tmp/../bar/../..")] + [InlineData("/tmp/bar")] + [InlineData("/tmp/bar/..")] public void GePathRoot_Unix(string path) { - var expected = @"/"; + var expected = "/"; Assert.Equal(expected, _fileSystem.Path.GetPathRoot(path)); PathAssert.Equal(expected.AsSpan(), _fileSystem.Path.GetPathRoot(path.AsSpan())); } public static TheoryData TestData_GetPathRoot_Windows => new() { - { @"C:", @"C:" }, + { "C:", "C:" }, { @"C:\", @"C:\" }, { @"C:\\", @"C:\" }, { @"C:\foo1", @"C:\" }, diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs index a76cfa6..3beb813 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs @@ -12,30 +12,30 @@ public class GetRelativePathExTest private readonly IFileSystem _fileSystem = new RealFileSystem(); [PlatformSpecificTheory(TestPlatformIdentifier.Windows)] - [InlineData(@"C:\", @"C:\", @".")] - [InlineData(@"C:\a", @"C:\a\", @".")] - [InlineData(@"C:\A", @"C:\a\", @".")] - [InlineData(@"C:\a\", @"C:\a", @".")] - [InlineData(@"C:\", @"C:\b", @"b")] + [InlineData(@"C:\", @"C:\", ".")] + [InlineData(@"C:\a", @"C:\a\", ".")] + [InlineData(@"C:\A", @"C:\a\", ".")] + [InlineData(@"C:\a\", @"C:\a", ".")] + [InlineData(@"C:\", @"C:\b", "b")] [InlineData(@"C:\a", @"C:\b", @"..\b")] [InlineData(@"C:\a", @"C:\b\", @"..\b\")] - [InlineData(@"C:\a\b", @"C:\a", @"..")] - [InlineData(@"C:\a\b", @"C:\a\", @"..")] - [InlineData(@"C:\a\b\", @"C:\a", @"..")] - [InlineData(@"C:\a\b\", @"C:\a\", @"..")] - [InlineData(@"C:\a\b\c", @"C:\a\b", @"..")] - [InlineData(@"C:\a\b\c", @"C:\a\b\", @"..")] + [InlineData(@"C:\a\b", @"C:\a", "..")] + [InlineData(@"C:\a\b", @"C:\a\", "..")] + [InlineData(@"C:\a\b\", @"C:\a", "..")] + [InlineData(@"C:\a\b\", @"C:\a\", "..")] + [InlineData(@"C:\a\b\c", @"C:\a\b", "..")] + [InlineData(@"C:\a\b\c", @"C:\a\b\", "..")] [InlineData(@"C:\a\b\c", @"C:\a", @"..\..")] [InlineData(@"C:\a\b\c", @"C:\a\", @"..\..")] - [InlineData(@"C:\a\b\c\", @"C:\a\b", @"..")] - [InlineData(@"C:\a\b\c\", @"C:\a\b\", @"..")] + [InlineData(@"C:\a\b\c\", @"C:\a\b", "..")] + [InlineData(@"C:\a\b\c\", @"C:\a\b\", "..")] [InlineData(@"C:\a\b\c\", @"C:\a", @"..\..")] [InlineData(@"C:\a\b\c\", @"C:\a\", @"..\..")] [InlineData(@"C:\a\", @"C:\b", @"..\b")] - [InlineData(@"C:\a", @"C:\a\b", @"b")] - [InlineData(@"C:\a", @"C:\A\b", @"b")] + [InlineData(@"C:\a", @"C:\a\b", "b")] + [InlineData(@"C:\a", @"C:\A\b", "b")] [InlineData(@"C:\a", @"C:\b\c", @"..\b\c")] - [InlineData(@"C:\a\", @"C:\a\b", @"b")] + [InlineData(@"C:\a\", @"C:\a\b", "b")] [InlineData(@"C:\", @"D:\", @"D:\")] [InlineData(@"C:\", @"D:\b", @"D:\b")] [InlineData(@"C:\", @"D:\b\", @"D:\b\")] @@ -46,7 +46,7 @@ public class GetRelativePathExTest [InlineData(@"C:\", @"\\LOCALHOST\Share\b", @"\\LOCALHOST\Share\b")] [InlineData(@"\\LOCALHOST\Share\a", @"\\LOCALHOST\Share\b", @"..\b")] // Tests which don't exist from .NET runtime - [InlineData(@"C:\a", @"C:\a\.\.", @".")] + [InlineData(@"C:\a", @"C:\a\.\.", ".")] public void GetRelativePathEx_FromAbsolute_Windows(string root, string path, string expected) { var result = _fileSystem.Path.GetRelativePathEx(root, path); @@ -60,7 +60,7 @@ public void GetRelativePathEx_FromAbsolute_Windows(string root, string path, str } [PlatformSpecificTheory(TestPlatformIdentifier.Windows)] - [InlineData(@"C:\a", @"b", @"b")] + [InlineData(@"C:\a", "b", "b")] [InlineData(@"C:\a", @"a\b", @"a\b")] [InlineData(@"C:\a", @"a\..\b", @"a\..\b")] public void GetRelativePathEx_FromRelative_Windows(string root, string path, string expected) @@ -70,11 +70,11 @@ public void GetRelativePathEx_FromRelative_Windows(string root, string path, str } [PlatformSpecificTheory(TestPlatformIdentifier.Windows)] - [InlineData(@"C:\", @"C:a", @"current\a")] - [InlineData(@"C:\a", @"C:a", @"..\current\a")] + [InlineData(@"C:\", "C:a", @"current\a")] + [InlineData(@"C:\a", "C:a", @"..\current\a")] [InlineData(@"C:\a", @"C:a\", @"..\current\a\")] [InlineData(@"C:\a\b", @"C:a\b", @"..\..\current\a\b")] - [InlineData(@"C:\a", @"X:a", @"X:\a")] + [InlineData(@"C:\a", "X:a", @"X:\a")] public void GetRelativePathEx_FromDriveRelative_Windows(string root, string path, string expected) { var fileSystem = new MockFileSystem(); @@ -92,22 +92,22 @@ public void GetRelativePathEx_FromDriveRelative_Windows(string root, string path } [PlatformSpecificTheory(TestPlatformIdentifier.Linux)] - [InlineData("/", @"/", @".")] - [InlineData("/a", @"/a/", @".")] - [InlineData("/a/", @"/a", @".")] - [InlineData("/", @"/b", @"b")] - [InlineData("/a", @"/b", @"../b")] - [InlineData("/a/", @"/b", @"../b")] - [InlineData("/a", @"/a/b", @"b")] - [InlineData("/a", @"/b/c", @"../b/c")] - [InlineData("/a/", @"/a/b", @"b")] - [InlineData("/ab", @"/a", @"../a")] - [InlineData("/a", @"/ab", @"../ab")] - [InlineData("/a", @"/A/", @"../A/")] - [InlineData("/a/", @"/A", @"../A")] - [InlineData("/a/", @"/A/b", @"../A/b")] + [InlineData("/", "/", ".")] + [InlineData("/a", "/a/", ".")] + [InlineData("/a/", "/a", ".")] + [InlineData("/", "/b", "b")] + [InlineData("/a", "/b", "../b")] + [InlineData("/a/", "/b", "../b")] + [InlineData("/a", "/a/b", "b")] + [InlineData("/a", "/b/c", "../b/c")] + [InlineData("/a/", "/a/b", "b")] + [InlineData("/ab", "/a", "../a")] + [InlineData("/a", "/ab", "../ab")] + [InlineData("/a", "/A/", "../A/")] + [InlineData("/a/", "/A", "../A")] + [InlineData("/a/", "/A/b", "../A/b")] // Tests which don't exist from .NET runtime - [InlineData(@"/a", @"/a/./.", @".")] + [InlineData("/a", "/a/./.", ".")] public void GetRelativePathEx_FromAbsolute_Linux(string root, string path, string expected) { var result = _fileSystem.Path.GetRelativePathEx(root, path); diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs index 27fd8af..e33c622 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs @@ -3,6 +3,7 @@ using AnakinRaW.CommonUtilities.Testing; using Testably.Abstractions; using Xunit; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.FileSystem.Test; @@ -25,19 +26,19 @@ public void HasLeadingPathSeparator(string? input) public static TheoryData TestData_StartsWithDirectorySeparator_Windows => new() { { @"\", true }, - { @"/", true }, + { "/", true }, { @"C:\folder\", false }, - { @"C:/folder/", false }, + { "C:/folder/", false }, { @"C:\", false }, - { @"C:/", false }, + { "C:/", false }, { @"\\", true }, - { @"//", true }, + { "//", true }, { @"\\server\share\", true }, { @"\\?\UNC\a\", true }, { @"\\?\C:\", true }, { @"\\?\UNC\", true }, { @"\folder", true }, - { @"folder", false }, + { "folder", false }, }; [PlatformSpecificTheory(TestPlatformIdentifier.Windows)] @@ -51,11 +52,11 @@ public void HasLeadingPathSeparator_Windows(string input, bool expected) public static TheoryData TestData_StartsWithDirectorySeparator_Linux => new() { - { @"/", true }, - { @"/folder/", true }, - { @"//", true }, - { @"folder", false }, - { @"/folder", true } + { "/", true }, + { "/folder/", true }, + { "//", true }, + { "folder", false }, + { "/folder", true } }; [PlatformSpecificTheory(TestPlatformIdentifier.Linux)] diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs index b9e555e..3422769 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs @@ -6,6 +6,7 @@ #if NET using System.IO; #endif +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.FileSystem.Test; @@ -31,19 +32,19 @@ public void HasTrailingPathSeparator(string? input) public static TheoryData TestData_EndsInDirectorySeparator_Windows => new() { { @"\", true }, - { @"/", true }, + { "/", true }, { @"C:\folder\", true }, - { @"C:/folder/", true }, + { "C:/folder/", true }, { @"C:\", true }, - { @"C:/", true }, + { "C:/", true }, { @"\\", true }, - { @"//", true }, + { "//", true }, { @"\\server\share\", true }, { @"\\?\UNC\a\", true }, { @"\\?\C:\", true }, { @"\\?\UNC\", true }, { @"folder\", true }, - { @"folder", false }, + { "folder", false }, }; [PlatformSpecificTheory(TestPlatformIdentifier.Windows)] @@ -59,11 +60,11 @@ public void HasTrailingPathSeparator_Windows(string input, bool expected) public static TheoryData TestData_EndsInDirectorySeparator_Linux => new() { - { @"/", true }, - { @"/folder/", true }, - { @"//", true }, - { @"folder", false }, - { @"folder/", true } + { "/", true }, + { "/folder/", true }, + { "//", true }, + { "folder", false }, + { "folder/", true } }; [PlatformSpecificTheory(TestPlatformIdentifier.Linux)] diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs index 663c5ec..71886a3 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs @@ -46,10 +46,10 @@ public void IsPathFullyQualified_Windows_Invalid(string path) [InlineData(@"C:\foo1")] [InlineData(@"C:\\")] [InlineData(@"C:\\foo2")] - [InlineData(@"C:/")] - [InlineData(@"C:/foo1")] - [InlineData(@"C://")] - [InlineData(@"C://foo2")] + [InlineData("C:/")] + [InlineData("C:/foo1")] + [InlineData("C://")] + [InlineData("C://foo2")] public void IsPathFullyQualified_Windows_Valid(string path) { Assert.True(_fileSystem.Path.IsPathFullyQualified(path)); @@ -63,9 +63,9 @@ public void IsPathFullyQualified_Windows_Valid(string path) [InlineData("./foo.txt")] [InlineData("..")] [InlineData("../foo.txt")] - [InlineData(@"C:")] - [InlineData(@"C:/")] - [InlineData(@"C://")] + [InlineData("C:")] + [InlineData("C:/")] + [InlineData("C://")] public void IsPathFullyQualified_Unix_Invalid(string path) { Assert.False(_fileSystem.Path.IsPathFullyQualified(path)); diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs index 6b3f819..9cafe96 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs @@ -3,6 +3,7 @@ using System.IO.Abstractions; using Testably.Abstractions; using Xunit; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.FileSystem.Test; diff --git a/src/CommonUtilities.FileSystem/test/Utilities/ValueStringBuilderTest.cs b/src/CommonUtilities.FileSystem/test/Utilities/ValueStringBuilderTest.cs index fd1987a..b5b4cf7 100644 --- a/src/CommonUtilities.FileSystem/test/Utilities/ValueStringBuilderTest.cs +++ b/src/CommonUtilities.FileSystem/test/Utilities/ValueStringBuilderTest.cs @@ -46,7 +46,7 @@ public void Append_Char_MatchesStringBuilder() { var sb = new StringBuilder(); var vsb = new ValueStringBuilder(); - for (int i = 1; i <= 100; i++) + for (var i = 1; i <= 100; i++) { sb.Append((char)i); vsb.Append((char)i); @@ -61,9 +61,9 @@ public void Append_String_MatchesStringBuilder() { var sb = new StringBuilder(); var vsb = new ValueStringBuilder(); - for (int i = 1; i <= 100; i++) + for (var i = 1; i <= 100; i++) { - string s = i.ToString(); + var s = i.ToString(); sb.Append(s); vsb.Append(s); } @@ -81,7 +81,7 @@ public void Append_String_Large_MatchesStringBuilder(int initialLength, int stri var sb = new StringBuilder(initialLength); var vsb = new ValueStringBuilder(new char[initialLength]); - string s = new string('a', stringLength); + var s = new string('a', stringLength); sb.Append(s); vsb.Append(s); @@ -94,7 +94,7 @@ public void Append_CharInt_MatchesStringBuilder() { var sb = new StringBuilder(); var vsb = new ValueStringBuilder(); - for (int i = 1; i <= 100; i++) + for (var i = 1; i <= 100; i++) { sb.Append((char)i, i); vsb.Append((char)i, i); @@ -109,9 +109,9 @@ public unsafe void Append_PtrInt_MatchesStringBuilder() { var sb = new StringBuilder(); var vsb = new ValueStringBuilder(); - for (int i = 1; i <= 100; i++) + for (var i = 1; i <= 100; i++) { - string s = i.ToString(); + var s = i.ToString(); fixed (char* p = s) { sb.Append(p, s.Length); @@ -129,13 +129,13 @@ public void AppendSpan_DataAppendedCorrectly() var sb = new StringBuilder(); var vsb = new ValueStringBuilder(); - for (int i = 1; i <= 1000; i++) + for (var i = 1; i <= 1000; i++) { - string s = i.ToString(); + var s = i.ToString(); sb.Append(s); - Span span = vsb.AppendSpan(s.Length); + var span = vsb.AppendSpan(s.Length); Assert.Equal(sb.Length, vsb.Length); s.AsSpan().CopyTo(span); @@ -152,9 +152,9 @@ public void Insert_IntCharInt_MatchesStringBuilder() var vsb = new ValueStringBuilder(); var rand = new Random(42); - for (int i = 1; i <= 100; i++) + for (var i = 1; i <= 100; i++) { - int index = rand.Next(sb.Length); + var index = rand.Next(sb.Length); sb.Insert(index, new string((char)i, 1), i); vsb.Insert(index, (char)i, i); } @@ -169,9 +169,9 @@ public void AsSpan_ReturnsCorrectValue_DoesntClearBuilder() var sb = new StringBuilder(); var vsb = new ValueStringBuilder(); - for (int i = 1; i <= 100; i++) + for (var i = 1; i <= 100; i++) { - string s = i.ToString(); + var s = i.ToString(); sb.Append(s); vsb.Append(s); } @@ -187,23 +187,23 @@ public void AsSpan_ReturnsCorrectValue_DoesntClearBuilder() [Fact] public void ToString_ClearsBuilder_ThenReusable() { - const string Text1 = "test"; + const string text1 = "test"; var vsb = new ValueStringBuilder(); - vsb.Append(Text1); - Assert.Equal(Text1.Length, vsb.Length); + vsb.Append(text1); + Assert.Equal(text1.Length, vsb.Length); - string s = vsb.ToString(); - Assert.Equal(Text1, s); + var s = vsb.ToString(); + Assert.Equal(text1, s); Assert.Equal(0, vsb.Length); Assert.Equal(string.Empty, vsb.ToString()); Assert.True(vsb.TryCopyTo(Span.Empty, out _)); - const string Text2 = "another test"; - vsb.Append(Text2); - Assert.Equal(Text2.Length, vsb.Length); - Assert.Equal(Text2, vsb.ToString()); + const string text2 = "another test"; + vsb.Append(text2); + Assert.Equal(text2.Length, vsb.Length); + Assert.Equal(text2, vsb.ToString()); } [Fact] @@ -211,12 +211,12 @@ public void TryCopyTo_FailsWhenDestinationIsTooSmall_SucceedsWhenItsLargeEnough( { var vsb = new ValueStringBuilder(); - const string Text = "expected text"; - vsb.Append(Text); - Assert.Equal(Text.Length, vsb.Length); + const string text = "expected text"; + vsb.Append(text); + Assert.Equal(text.Length, vsb.Length); - Span dst = new char[Text.Length - 1]; - Assert.False(vsb.TryCopyTo(dst, out int charsWritten)); + Span dst = new char[text.Length - 1]; + Assert.False(vsb.TryCopyTo(dst, out var charsWritten)); Assert.Equal(0, charsWritten); Assert.Equal(0, vsb.Length); } @@ -224,35 +224,35 @@ public void TryCopyTo_FailsWhenDestinationIsTooSmall_SucceedsWhenItsLargeEnough( [Fact] public void TryCopyTo_ClearsBuilder_ThenReusable() { - const string Text1 = "test"; + const string text1 = "test"; var vsb = new ValueStringBuilder(); - vsb.Append(Text1); - Assert.Equal(Text1.Length, vsb.Length); + vsb.Append(text1); + Assert.Equal(text1.Length, vsb.Length); - Span dst = new char[Text1.Length]; - Assert.True(vsb.TryCopyTo(dst, out int charsWritten)); - Assert.Equal(Text1.Length, charsWritten); - Assert.Equal(Text1, dst.ToString()); + Span dst = new char[text1.Length]; + Assert.True(vsb.TryCopyTo(dst, out var charsWritten)); + Assert.Equal(text1.Length, charsWritten); + Assert.Equal(text1, dst.ToString()); Assert.Equal(0, vsb.Length); Assert.Equal(string.Empty, vsb.ToString()); Assert.True(vsb.TryCopyTo(Span.Empty, out _)); - const string Text2 = "another test"; - vsb.Append(Text2); - Assert.Equal(Text2.Length, vsb.Length); - Assert.Equal(Text2, vsb.ToString()); + const string text2 = "another test"; + vsb.Append(text2); + Assert.Equal(text2.Length, vsb.Length); + Assert.Equal(text2, vsb.ToString()); } [Fact] public void Dispose_ClearsBuilder_ThenReusable() { - const string Text1 = "test"; + const string text1 = "test"; var vsb = new ValueStringBuilder(); - vsb.Append(Text1); - Assert.Equal(Text1.Length, vsb.Length); + vsb.Append(text1); + Assert.Equal(text1.Length, vsb.Length); vsb.Dispose(); @@ -260,19 +260,19 @@ public void Dispose_ClearsBuilder_ThenReusable() Assert.Equal(string.Empty, vsb.ToString()); Assert.True(vsb.TryCopyTo(Span.Empty, out _)); - const string Text2 = "another test"; - vsb.Append(Text2); - Assert.Equal(Text2.Length, vsb.Length); - Assert.Equal(Text2, vsb.ToString()); + const string text2 = "another test"; + vsb.Append(text2); + Assert.Equal(text2.Length, vsb.Length); + Assert.Equal(text2, vsb.ToString()); } [Fact] - public unsafe void Indexer() + public void Indexer() { - const string Text1 = "foobar"; + const string text1 = "foobar"; var vsb = new ValueStringBuilder(); - vsb.Append(Text1); + vsb.Append(text1); Assert.Equal('b', vsb[3]); vsb[3] = 'c'; diff --git a/src/CommonUtilities.Registry/src/IRegistryKey.cs b/src/CommonUtilities.Registry/src/IRegistryKey.cs index d782643..6407051 100644 --- a/src/CommonUtilities.Registry/src/IRegistryKey.cs +++ b/src/CommonUtilities.Registry/src/IRegistryKey.cs @@ -4,8 +4,7 @@ namespace AnakinRaW.CommonUtilities.Registry; /// -/// High-Level abstraction layer for the a Registry Key implementation. -/// Read and write operations are supported. +/// Represents a key node in of an . /// public interface IRegistryKey : IDisposable { diff --git a/src/CommonUtilities.Registry/src/InMemoryRegistryKeyData.cs b/src/CommonUtilities.Registry/src/InMemoryRegistryKeyData.cs index f3305d6..cf9cc25 100644 --- a/src/CommonUtilities.Registry/src/InMemoryRegistryKeyData.cs +++ b/src/CommonUtilities.Registry/src/InMemoryRegistryKeyData.cs @@ -242,7 +242,7 @@ public override void Dispose() var subKeyNames = subPath.Split(Separator); foreach (var subKeyName in subKeyNames) { - if (currentKey._subKeys.TryGetValue(subKeyName, out var key) == false) + if (!currentKey._subKeys.TryGetValue(subKeyName, out var key)) return null; currentKey = key; } diff --git a/src/CommonUtilities.Registry/test/Extensions/InMemoryKeyExtensionsTest.cs b/src/CommonUtilities.Registry/test/Extensions/InMemoryKeyExtensionsTest.cs index 88a576d..ecef9a3 100644 --- a/src/CommonUtilities.Registry/test/Extensions/InMemoryKeyExtensionsTest.cs +++ b/src/CommonUtilities.Registry/test/Extensions/InMemoryKeyExtensionsTest.cs @@ -1,5 +1,6 @@ namespace AnakinRaW.CommonUtilities.Registry.Test.Extensions; +// ReSharper disable once UnusedMember.Global public class InMemoryKeyExtensionsTest : RegistryKeyExtensionsTestBase { protected override RegKeyTest CreateTestKey() diff --git a/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs b/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs index b74e355..fa8b58d 100644 --- a/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs +++ b/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs @@ -382,7 +382,7 @@ public async Task AwaitRegKeyChange_CallingThreadDestroyed() thread.Join(); // Verify that the watching task is still watching. - var completedTask = await Task.WhenAny(watchingTask!, Task.Delay(AsyncDelay)); + var completedTask = await Task.WhenAny(watchingTask, Task.Delay(AsyncDelay)); Assert.NotSame(watchingTask, completedTask); test.CreateSubKey().Dispose(); diff --git a/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs b/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs index 6d8c056..ad03bf7 100644 --- a/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs +++ b/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs @@ -4,6 +4,7 @@ namespace AnakinRaW.CommonUtilities.Registry.Test.Extensions; +// ReSharper disable once UnusedMember.Global public class WindowsKeyExtensionsTest : RegistryKeyExtensionsTestBase { protected override RegKeyTest CreateTestKey() diff --git a/src/CommonUtilities.Registry/test/RegistryKey_GetValueOrDefault.cs b/src/CommonUtilities.Registry/test/RegistryKey_GetValueOrDefault.cs index c7c7db9..6f44e79 100644 --- a/src/CommonUtilities.Registry/test/RegistryKey_GetValueOrDefault.cs +++ b/src/CommonUtilities.Registry/test/RegistryKey_GetValueOrDefault.cs @@ -57,7 +57,7 @@ public void GetValueOrDefault_GetByteArrayTest() byte[] expected = [1, 2, 3]; TestRegistryKey.SetValue(valueName, expected); - Assert.Equal(expected, TestRegistryKey.GetValueOrDefault(valueName, [0, 0], out var exists)); + Assert.Equal(expected, TestRegistryKey.GetValueOrDefault(valueName, "\0\0"u8.ToArray(), out var exists)); Assert.True(exists); TestRegistryKey.DeleteValue(valueName); } diff --git a/src/CommonUtilities.Registry/test/RegistryKey_GetValueOrSetDefault.cs b/src/CommonUtilities.Registry/test/RegistryKey_GetValueOrSetDefault.cs index 1d8ea27..104d9e3 100644 --- a/src/CommonUtilities.Registry/test/RegistryKey_GetValueOrSetDefault.cs +++ b/src/CommonUtilities.Registry/test/RegistryKey_GetValueOrSetDefault.cs @@ -84,7 +84,7 @@ public void GetValueOrSetDefault_GetByteArrayTest() byte[] expected = [1, 2, 3]; TestRegistryKey.SetValue(valueName, expected); - Assert.Equal(expected, TestRegistryKey.GetValueOrSetDefault(valueName, [0, 0], out var defaultUsed)); + Assert.Equal(expected, TestRegistryKey.GetValueOrSetDefault(valueName, "\0\0"u8.ToArray(), out var defaultUsed)); Assert.False(defaultUsed); TestRegistryKey.DeleteValue(valueName); } diff --git a/src/CommonUtilities.Registry/test/RegistryTestsBase.cs b/src/CommonUtilities.Registry/test/RegistryTestsBase.cs index 8ced84a..cc7af33 100644 --- a/src/CommonUtilities.Registry/test/RegistryTestsBase.cs +++ b/src/CommonUtilities.Registry/test/RegistryTestsBase.cs @@ -50,14 +50,14 @@ public void Dispose() public static readonly object[][] TestRegistrySubKeyNames = [ - [@"Foo", @"Foo"], + ["Foo", "Foo"], [@"Foo\Bar", @"Foo\Bar"], // Multiple/trailing slashes should be removed. - [@"Foo", @"Foo\"], - [@"Foo", @"Foo\\"], - [@"Foo", @"Foo\\\"], - [@"Foo", @"Foo\\\\"], + ["Foo", @"Foo\"], + ["Foo", @"Foo\\"], + ["Foo", @"Foo\\\"], + ["Foo", @"Foo\\\\"], [@"Foo\Bar", @"Foo\\Bar"], [@"Foo\Bar", @"Foo\\\Bar"], [@"Foo\Bar", @"Foo\\\\Bar"], @@ -74,16 +74,16 @@ public void Dispose() // If there are multiple slashes, any extra slash chars will be // replaced with a marker char ('\uffff'), and then all '\uffff' // chars will be removed, including any pre-existing '\uffff' chars. - InsertMarkerChar(@"Foo", @"{0}Foo\\"), - InsertMarkerChar(@"Foo", @"Foo{0}\\"), - InsertMarkerChar(@"Foo", @"Foo\\{0}"), - InsertMarkerChar(@"Foo", @"Fo{0}o\\"), - InsertMarkerChar(@"Foo", @"{0}Fo{0}o{0}\\{0}"), - InsertMarkerChar(@"Foo", @"{0}Foo\\\"), - InsertMarkerChar(@"Foo", @"Foo{0}\\\"), - InsertMarkerChar(@"Foo", @"Foo\\\{0}"), - InsertMarkerChar(@"Foo", @"Fo{0}o\\\"), - InsertMarkerChar(@"Foo", @"{0}Fo{0}o{0}\\\{0}"), + InsertMarkerChar("Foo", @"{0}Foo\\"), + InsertMarkerChar("Foo", @"Foo{0}\\"), + InsertMarkerChar("Foo", @"Foo\\{0}"), + InsertMarkerChar("Foo", @"Fo{0}o\\"), + InsertMarkerChar("Foo", @"{0}Fo{0}o{0}\\{0}"), + InsertMarkerChar("Foo", @"{0}Foo\\\"), + InsertMarkerChar("Foo", @"Foo{0}\\\"), + InsertMarkerChar("Foo", @"Foo\\\{0}"), + InsertMarkerChar("Foo", @"Fo{0}o\\\"), + InsertMarkerChar("Foo", @"{0}Fo{0}o{0}\\\{0}"), InsertMarkerChar(@"Foo\Bar", @"{0}Foo\\Bar"), InsertMarkerChar(@"Foo\Bar", @"Foo{0}\\Bar"), InsertMarkerChar(@"Foo\Bar", @"Foo\\{0}Bar"), @@ -109,10 +109,10 @@ public void Dispose() InsertMarkerChar(@"Foo\Bar", @"{0}Fo{0}o{0}\{0}B{0}ar{0}\\{0}"), // If there aren't multiple slashes, any '\uffff' chars should remain. - InsertMarkerChar(@"{0}Foo"), - InsertMarkerChar(@"Foo{0}"), - InsertMarkerChar(@"Fo{0}o"), - InsertMarkerChar(@"{0}Fo{0}o{0}"), + InsertMarkerChar("{0}Foo"), + InsertMarkerChar("Foo{0}"), + InsertMarkerChar("Fo{0}o"), + InsertMarkerChar("{0}Fo{0}o{0}"), InsertMarkerChar(@"{0}Foo\"), InsertMarkerChar(@"Foo{0}\"), InsertMarkerChar(@"Fo{0}o\"), @@ -155,18 +155,18 @@ protected void Verify_CreateSubKey_KeyExists_OpensKeyWithFixedUpName(string expe using var key = createSubKey(); Assert.NotNull(key); - Assert.Single(TestRegistryKey.GetSubKeyNames()!); + Assert.Single(TestRegistryKey.GetSubKeyNames()); Assert.Equal(TestRegistryKey.Name + @"\" + expected, key.Name); } protected void Verify_CreateSubKey_KeyDoesNotExist_CreatesKeyWithFixedUpName(string expected, Func createSubKey) { Assert.Null(TestRegistryKey.OpenSubKey(expected)); - Assert.Empty(TestRegistryKey.GetSubKeyNames()!); + Assert.Empty(TestRegistryKey.GetSubKeyNames()); using var key = createSubKey(); Assert.NotNull(key); - Assert.Single(TestRegistryKey.GetSubKeyNames()!); + Assert.Single(TestRegistryKey.GetSubKeyNames()); Assert.Equal(TestRegistryKey.Name + @"\" + expected, key.Name); } @@ -181,7 +181,7 @@ protected void Verify_DeleteSubKey_KeyExists_KeyDeleted(string expected, Action protected void Verify_DeleteSubKey_KeyDoesNotExists_DoesNotThrow(string expected, Action deleteSubKey) { Assert.Null(TestRegistryKey.OpenSubKey(expected)); - Assert.Empty(TestRegistryKey.GetSubKeyNames()!); + Assert.Empty(TestRegistryKey.GetSubKeyNames()); deleteSubKey(); } @@ -192,14 +192,14 @@ protected void Verify_OpenSubKey_KeyExists_OpensWithFixedUpName(string expected, using var key = openSubKey(); Assert.NotNull(key); - Assert.Single(TestRegistryKey.GetSubKeyNames()!); + Assert.Single(TestRegistryKey.GetSubKeyNames()); Assert.Equal(TestRegistryKey.Name + @"\" + expected, key.Name); } protected void Verify_OpenSubKey_KeyDoesNotExist_ReturnsNull(string expected, Func openSubKey) { Assert.Null(TestRegistryKey.OpenSubKey(expected)); - Assert.Empty(TestRegistryKey.GetSubKeyNames()!); + Assert.Empty(TestRegistryKey.GetSubKeyNames()); Assert.Null(openSubKey()); } diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs b/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs index b29f634..d692c9e 100644 --- a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs +++ b/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs @@ -20,7 +20,7 @@ public abstract class StepRunnerBase : IStepRunner /// /// Gets a modifiable bag of all executed steps. /// - protected readonly ConcurrentBag ExecutedStepsBag = new(); + protected readonly ConcurrentBag ExecutedStepsBag = []; /// /// Gets the logger instance of this stepRunner. diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs index b977ef1..acaac39 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs @@ -20,6 +20,7 @@ protected override void RunSynchronized(CancellationToken token) Logger?.LogTrace($"Running {_pipeline}..."); try { + // ReSharper disable once MethodSupportsCancellation _pipeline.RunAsync(token).Wait(); Logger?.LogTrace($"Finished {_pipeline}"); } diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs index c8f73bd..744dd01 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs @@ -5,6 +5,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; +// ReSharper disable once UnusedMember.Global public class ParallelPipelineTests : StepRunnerPipelineTest { protected override Pipeline CreatePipeline(IList steps) diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs index 3691e8e..e899338 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs @@ -77,7 +77,9 @@ public async Task RunAsync_DelayedAdd_PrepareFails(bool failFast) return; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously async IAsyncEnumerable ValueFunction() +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { yield return s1; yield return s2; @@ -97,7 +99,9 @@ public async Task PrepareAsync_PrepareFails() return; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously async IAsyncEnumerable ValueFunction() +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { yield return s1; yield return s2; diff --git a/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs b/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs index 68b030a..099142d 100644 --- a/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs @@ -3,9 +3,11 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Progress; using AnakinRaW.CommonUtilities.Testing; using Xunit; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Progress; +// ReSharper disable once UnusedMember.Global public class AggregatedProgressReporterTest_Struct : AggregatedProgressReporterTestBase { protected override TestInfoStruct CreateCustomProgressInfo(TestProgressStep step, double progress) @@ -17,6 +19,7 @@ protected override TestInfoStruct CreateCustomProgressInfo(TestProgressStep { protected override TestInfoClass CreateCustomProgressInfo(TestProgressStep step, double progress) diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs index 2dfe181..019f5b6 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs @@ -29,7 +29,7 @@ public void Wait() var step = new WaitStep(runner, ServiceProvider); - var runnerTask = runner.RunAsync(CancellationToken.None); + _ = runner.RunAsync(CancellationToken.None); step.Run(CancellationToken.None); // We cannot assert on the runnerTask task, diff --git a/src/CommonUtilities.SimplePipeline/test/TestStep.cs b/src/CommonUtilities.SimplePipeline/test/TestStep.cs index 6f3ac5f..bfef8bb 100644 --- a/src/CommonUtilities.SimplePipeline/test/TestStep.cs +++ b/src/CommonUtilities.SimplePipeline/test/TestStep.cs @@ -88,7 +88,7 @@ public override int GetHashCode() } } -public class TestSyncStep(Action action, IServiceProvider serviceProvider) +public class TestSyncStep(Action? action, IServiceProvider serviceProvider) : SynchronizedStep(serviceProvider) { public ProgressType Type => new() { Id = "test", DisplayName = "Test" }; diff --git a/src/CommonUtilities.TestingUtilities/Collections/CollectionsTestSuite.cs b/src/CommonUtilities.TestingUtilities/Collections/CollectionsTestSuite.cs index 02cda4e..1fc1d51 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/CollectionsTestSuite.cs +++ b/src/CommonUtilities.TestingUtilities/Collections/CollectionsTestSuite.cs @@ -96,8 +96,8 @@ protected IEnumerable CreateEnumerable(IEnumerable? enumerableToMatchTo, i } /// - /// Helper function to create an List fulfilling the given specific parameters. The function will - /// create an List and then add values + /// Helper function to create a List fulfilling the given specific parameters. The function will + /// create a List and then add values /// to it until it is full. It will begin by adding the desired number of matching, /// followed by random (deterministic) elements until the desired count is reached. /// diff --git a/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs b/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs index 431ab5b..cf077a4 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs +++ b/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs @@ -14,7 +14,7 @@ public abstract class ICollectionTestSuite : IEnumerableTestSuite { protected virtual Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentException); - protected virtual IEnumerable InvalidValues => Array.Empty(); + protected virtual IEnumerable InvalidValues => []; protected virtual bool DefaultValueAllowed => true; diff --git a/src/CommonUtilities.TestingUtilities/Collections/INonModifyingEnumerableTestSuite.cs b/src/CommonUtilities.TestingUtilities/Collections/INonModifyingEnumerableTestSuite.cs index 0bf2a39..a273bd5 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/INonModifyingEnumerableTestSuite.cs +++ b/src/CommonUtilities.TestingUtilities/Collections/INonModifyingEnumerableTestSuite.cs @@ -368,8 +368,10 @@ public void IEnumerable_Generic_Enumerator_Current_ReturnsSameObjectsOnDifferent // Ensures that the elements returned from enumeration are exactly the same collection of // elements returned from a previous enumeration var enumerable = GenericIEnumerableFactory(count); +#pragma warning disable CS8714 var firstValues = new Dictionary(count); var secondValues = new Dictionary(count); +#pragma warning restore CS8714 foreach (var item in enumerable) firstValues[item] = firstValues.ContainsKey(item) ? firstValues[item]++ : 1; foreach (var item in enumerable) diff --git a/src/CommonUtilities/src/Collections/FrugalList.cs b/src/CommonUtilities/src/Collections/FrugalList.cs index 048757d..b4e489c 100644 --- a/src/CommonUtilities/src/Collections/FrugalList.cs +++ b/src/CommonUtilities/src/Collections/FrugalList.cs @@ -44,11 +44,11 @@ namespace AnakinRaW.CommonUtilities.Collections; /// /// Usage advise: /// -/// a) To ensure that all changes get reflected to other variables (including the first item) +/// a. To ensure that all changes get reflected to other variables (including the first item) /// either box this structure (e.g, to [this allocates memory though]) or pass this structure as by-. /// /// -/// b) If a copy shall not reflect any changes from its source use +/// b. If a copy shall not reflect any changes from its source use /// which creates a full shallow-copy of all items in this list. /// /// @@ -292,9 +292,9 @@ public void RemoveAt(int index) // Natively implementing frequent Linq functions avoids boxing. Add more if necessary. /// - /// Creates a from an this instance. + /// Creates a from the . /// - /// A that contains elements from the this list. + /// A that contains elements from the . public readonly List ToList() { if (_tailList is null) @@ -319,7 +319,7 @@ public readonly T[] ToArray() } /// - /// Returns the first element of of the . + /// Returns the first element of the . /// /// The first element of the specified /// The is empty. diff --git a/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs b/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs index 5d2fa81..7b5119a 100644 --- a/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs +++ b/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs @@ -65,9 +65,9 @@ public void CopyTo(T[] array, int index) // Natively implementing frequent Linq functions avoids boxing. Add more if necessary. /// - /// Creates a from an this instance. + /// Creates a from the . /// - /// A that contains elements from this list. + /// A that contains elements from the . public List ToList() { return _list.ToList(); @@ -146,11 +146,16 @@ public int IndexOf(T item) /// Returns an enumerator that iterates through the /// /// A for the . - public FrugalList.FrugalEnumerator GetEnumerator() => _list.GetEnumerator(); + public FrugalList.FrugalEnumerator GetEnumerator() + { + // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable + return _list.GetEnumerator(); + } /// IEnumerator IEnumerable.GetEnumerator() { + // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable return _list.GetEnumerator(); } diff --git a/src/CommonUtilities/src/Hashing/HashTypeKey.cs b/src/CommonUtilities/src/Hashing/HashTypeKey.cs index adae1fc..946da33 100644 --- a/src/CommonUtilities/src/Hashing/HashTypeKey.cs +++ b/src/CommonUtilities/src/Hashing/HashTypeKey.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.Hashing; diff --git a/src/CommonUtilities/src/Hashing/Providers/MD5HashProvider.cs b/src/CommonUtilities/src/Hashing/Providers/MD5HashProvider.cs index 0e26119..d64bd69 100644 --- a/src/CommonUtilities/src/Hashing/Providers/MD5HashProvider.cs +++ b/src/CommonUtilities/src/Hashing/Providers/MD5HashProvider.cs @@ -6,6 +6,7 @@ namespace AnakinRaW.CommonUtilities.Hashing.Providers; +// ReSharper disable once InconsistentNaming internal class MD5HashProvider : HashAlgorithmProviderBase { public override HashTypeKey SupportedHashType => HashTypeKey.MD5; diff --git a/src/CommonUtilities/src/Hashing/Providers/SHA1HashProvider.cs b/src/CommonUtilities/src/Hashing/Providers/SHA1HashProvider.cs index 41c8997..fe0bc4a 100644 --- a/src/CommonUtilities/src/Hashing/Providers/SHA1HashProvider.cs +++ b/src/CommonUtilities/src/Hashing/Providers/SHA1HashProvider.cs @@ -6,6 +6,7 @@ namespace AnakinRaW.CommonUtilities.Hashing.Providers; +// ReSharper disable once InconsistentNaming internal class SHA1HashProvider : HashAlgorithmProviderBase { public override HashTypeKey SupportedHashType => HashTypeKey.SHA1; diff --git a/src/CommonUtilities/src/Hashing/Providers/SHA256HashProvider.cs b/src/CommonUtilities/src/Hashing/Providers/SHA256HashProvider.cs index 79756db..99082bc 100644 --- a/src/CommonUtilities/src/Hashing/Providers/SHA256HashProvider.cs +++ b/src/CommonUtilities/src/Hashing/Providers/SHA256HashProvider.cs @@ -6,6 +6,7 @@ namespace AnakinRaW.CommonUtilities.Hashing.Providers; +// ReSharper disable once InconsistentNaming internal class SHA256HashProvider : HashAlgorithmProviderBase { public override HashTypeKey SupportedHashType => HashTypeKey.SHA256; diff --git a/src/CommonUtilities/src/Hashing/Providers/SHA384HashProvider.cs b/src/CommonUtilities/src/Hashing/Providers/SHA384HashProvider.cs index 3c21e69..5198ba4 100644 --- a/src/CommonUtilities/src/Hashing/Providers/SHA384HashProvider.cs +++ b/src/CommonUtilities/src/Hashing/Providers/SHA384HashProvider.cs @@ -6,6 +6,7 @@ namespace AnakinRaW.CommonUtilities.Hashing.Providers; +// ReSharper disable once InconsistentNaming internal class SHA384HashProvider : HashAlgorithmProviderBase { public override HashTypeKey SupportedHashType => HashTypeKey.SHA384; diff --git a/src/CommonUtilities/src/Hashing/Providers/SHA512HashProvider.cs b/src/CommonUtilities/src/Hashing/Providers/SHA512HashProvider.cs index a0d7374..af086bf 100644 --- a/src/CommonUtilities/src/Hashing/Providers/SHA512HashProvider.cs +++ b/src/CommonUtilities/src/Hashing/Providers/SHA512HashProvider.cs @@ -6,6 +6,7 @@ namespace AnakinRaW.CommonUtilities.Hashing.Providers; +// ReSharper disable once InconsistentNaming internal class SHA512HashProvider : HashAlgorithmProviderBase { public override HashTypeKey SupportedHashType => HashTypeKey.SHA512; diff --git a/src/CommonUtilities/src/NativeMethods/AdvApi32.cs b/src/CommonUtilities/src/NativeMethods/AdvApi32.cs index 55a054e..5997599 100644 --- a/src/CommonUtilities/src/NativeMethods/AdvApi32.cs +++ b/src/CommonUtilities/src/NativeMethods/AdvApi32.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Runtime.InteropServices; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.NativeMethods; diff --git a/src/CommonUtilities/test/Collections/FrugalListTest.cs b/src/CommonUtilities/test/Collections/FrugalListTest.cs index d834dbd..264e320 100644 --- a/src/CommonUtilities/test/Collections/FrugalListTest.cs +++ b/src/CommonUtilities/test/Collections/FrugalListTest.cs @@ -1,4 +1,5 @@ using System; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.Test.Collections; diff --git a/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTests.cs b/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTests.cs index 7d38d7f..aca8c84 100644 --- a/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTests.cs +++ b/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using AnakinRaW.CommonUtilities.Collections; +// ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.Test.Collections; diff --git a/src/CommonUtilities/test/Extensions/EncodingExtensionsTest.cs b/src/CommonUtilities/test/Extensions/EncodingExtensionsTest.cs index 429ad52..dc35d93 100644 --- a/src/CommonUtilities/test/Extensions/EncodingExtensionsTest.cs +++ b/src/CommonUtilities/test/Extensions/EncodingExtensionsTest.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using AnakinRaW.CommonUtilities.Extensions; @@ -133,13 +134,14 @@ public void GetChars() #region EncodeString [Fact] + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] public void EncodeString_NullArgs_Throws() { Encoding encoding = null!; Assert.Throws(() => encoding.EncodeString("")); Assert.Throws(() => encoding.EncodeString("", 0)); - Assert.Throws(() => encoding!.EncodeString("".AsSpan(), Span.Empty)); - Assert.Throws(() => encoding!.EncodeString("".AsSpan(), Span.Empty, 0)); + Assert.Throws(() => encoding.EncodeString("".AsSpan(), Span.Empty)); + Assert.Throws(() => encoding.EncodeString("".AsSpan(), Span.Empty, 0)); ForEachEncoding(e => { diff --git a/src/CommonUtilities/test/Hashing/HashTypeKeyTest.cs b/src/CommonUtilities/test/Hashing/HashTypeKeyTest.cs index dbd0c50..1d68209 100644 --- a/src/CommonUtilities/test/Hashing/HashTypeKeyTest.cs +++ b/src/CommonUtilities/test/Hashing/HashTypeKeyTest.cs @@ -18,9 +18,9 @@ public void Ctor() public void None() { var key = HashTypeKey.None; - Assert.Null(key.Name); Assert.Equal(0, key.GetHashCode()); Assert.Equal(0, key.HashSize); + Assert.Null(key.Name); } [Fact] diff --git a/src/CommonUtilities/test/Hashing/HashingServiceTest.cs b/src/CommonUtilities/test/Hashing/HashingServiceTest.cs index d92e471..8082c67 100644 --- a/src/CommonUtilities/test/Hashing/HashingServiceTest.cs +++ b/src/CommonUtilities/test/Hashing/HashingServiceTest.cs @@ -197,7 +197,7 @@ public void GetHash_AlwaysOneProvider() var someSource = Array.Empty(); var someStream = new MemoryStream(someSource); - var destination = new byte[] { 0, 0 }; + var destination = "\0\0"u8.ToArray(); var expectedHashExact = new byte[] { 1 }; var expectedHashJoint = new byte[] { 1, 0 }; @@ -235,7 +235,7 @@ public async Task GetHashAsync_AlwaysOneProvider() var someSource = Array.Empty(); var someStream = new MemoryStream(someSource); - var destination = new byte[] { 0, 0 }; + var destination = "\0\0"u8.ToArray(); var expectedHashExact = new byte[] { 1 }; var expectedHashJoint = new byte[] { 1, 0 }; From e65029f4fdaa4a4e6ef5f4a1c81c3e850a903cc3 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 7 Dec 2025 18:30:40 +0100 Subject: [PATCH 19/43] extensions --- .../src/Extensions/FileSystemExtensions.cs | 96 ++++---- .../Extensions/FileSystemInfoExtensions.cs | 79 +++---- .../src/Windows/WindowsPathExtensions.cs | 93 ++++---- .../src/Extensions.cs | 17 +- .../EncodingExtensions.NetFramework.cs | 90 ++++---- .../src/Extensions/EncodingExtensions.cs | 217 +++++++++--------- 6 files changed, 298 insertions(+), 294 deletions(-) diff --git a/src/CommonUtilities.FileSystem/src/Extensions/FileSystemExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/FileSystemExtensions.cs index 032051d..289843e 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/FileSystemExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/FileSystemExtensions.cs @@ -9,58 +9,58 @@ namespace AnakinRaW.CommonUtilities.FileSystem; /// public static class FileSystemExtensions { - /// - /// Tries to create a new file and returns an open to the created file, or if the file could not be created. - /// An existing file will be overwritten. - /// /// - /// The file's location. - /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. - /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. - /// Number of retry attempts tempts until the operation fails. - /// Delay time in ms between each new attempt. - /// Open file stream or if the file could not be created. - /// or is . - public static FileSystemStream? CreateFileWithRetry( - this IFileSystem fs, - string path, - FileAccess fileAccess = FileAccess.ReadWrite, - FileShare fileShare = FileShare.None, - int retryCount = 2, - int retryDelay = 500) + extension(IFileSystem fs) { - if (fs == null) - throw new ArgumentNullException(nameof(fs)); - if (path == null) - throw new ArgumentNullException(nameof(path)); - - FileSystemStream? stream = null; - FileSystemUtilities.ExecuteFileSystemActionWithRetry(retryCount, retryDelay, - () => stream = fs.FileStream.New(path, FileMode.Create, fileAccess, fileShare)); - return stream; - } + /// + /// Tries to create a new file and returns an open to the created file, or if the file could not be created. + /// An existing file will be overwritten. + /// + /// The file's location. + /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. + /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. + /// Number of retry attempts tempts until the operation fails. + /// Delay time in ms between each new attempt. + /// Open file stream or if the file could not be created. + /// or is . + public FileSystemStream? CreateFileWithRetry(string path, + FileAccess fileAccess = FileAccess.ReadWrite, + FileShare fileShare = FileShare.None, + int retryCount = 2, + int retryDelay = 500) + { + if (fs == null) + throw new ArgumentNullException(nameof(fs)); + if (path == null) + throw new ArgumentNullException(nameof(path)); - /// - /// Tries to create a new unique folder within the current users temporary directory. - /// - /// - /// Number of retry attempts tempts until the operation fails. - /// Delay time in ms between each new attempt. - /// The of the created folder or . - /// is . - public static IDirectoryInfo? CreateTemporaryFolderInTempWithRetry(this IFileSystem fs, int retryCount = 2, int retryDelay = 500) - { - if (fs == null) - throw new ArgumentNullException(nameof(fs)); + FileSystemStream? stream = null; + FileSystemUtilities.ExecuteFileSystemActionWithRetry(retryCount, retryDelay, + () => stream = fs.FileStream.New(path, FileMode.Create, fileAccess, fileShare)); + return stream; + } - IDirectoryInfo? newTempFolder = null; - FileSystemUtilities.ExecuteFileSystemActionWithRetry(retryCount, retryDelay, () => + /// + /// Tries to create a new unique folder within the current users temporary directory. + /// + /// Number of retry attempts tempts until the operation fails. + /// Delay time in ms between each new attempt. + /// The of the created folder or . + /// is . + public IDirectoryInfo? CreateTemporaryFolderInTempWithRetry(int retryCount = 2, int retryDelay = 500) { - var tempFolder = fs.Path.GetTempPath(); - var folderName = fs.Path.GetRandomFileName(); - var fullFolderPath = fs.Path.Combine(tempFolder, folderName); - newTempFolder = fs.Directory.CreateDirectory(fullFolderPath); - }); - return newTempFolder; + if (fs == null) + throw new ArgumentNullException(nameof(fs)); + + IDirectoryInfo? newTempFolder = null; + FileSystemUtilities.ExecuteFileSystemActionWithRetry(retryCount, retryDelay, () => + { + var tempFolder = fs.Path.GetTempPath(); + var folderName = fs.Path.GetRandomFileName(); + var fullFolderPath = fs.Path.Combine(tempFolder, folderName); + newTempFolder = fs.Directory.CreateDirectory(fullFolderPath); + }); + return newTempFolder; + } } } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Extensions/FileSystemInfoExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/FileSystemInfoExtensions.cs index e9375ad..448737e 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/FileSystemInfoExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/FileSystemInfoExtensions.cs @@ -9,51 +9,52 @@ namespace AnakinRaW.CommonUtilities.FileSystem; /// public static class FileSystemInfoExtensions { - /// - /// Gets the remaining free bytes on the drive where is located. - /// /// Some file or directory at the targeted drive. - /// free drive space in bytes - /// is . - public static long GetDriveFreeSpace(this IFileSystemInfo fsItem) + extension(IFileSystemInfo fsItem) { - if (fsItem == null) - throw new ArgumentNullException(nameof(fsItem)); + /// + /// Gets the remaining free bytes on the drive where is located. + /// + /// free drive space in bytes + /// is . + public long GetDriveFreeSpace() + { + if (fsItem == null) + throw new ArgumentNullException(nameof(fsItem)); - var root = fsItem.FileSystem.Path.GetPathRoot(fsItem.FullName); - return fsItem.FileSystem.DriveInfo.New(root!).AvailableFreeSpace; - } + var root = fsItem.FileSystem.Path.GetPathRoot(fsItem.FullName); + return fsItem.FileSystem.DriveInfo.New(root!).AvailableFreeSpace; + } - /// - /// Removes attributes from a given filesystem entry. - /// - /// The target filesystem handle. - /// Attributes to remove. - /// is . - public static void RemoveAttributes(this IFileSystemInfo fsInfo, FileAttributes attributesToRemove) - { - if (fsInfo == null) - throw new ArgumentNullException(nameof(fsInfo)); + /// + /// Removes attributes from a given filesystem entry. + /// + /// Attributes to remove. + /// is . + public void RemoveAttributes(FileAttributes attributesToRemove) + { + if (fsItem == null) + throw new ArgumentNullException(nameof(fsItem)); - var currentAttributes = fsInfo.Attributes; - var newAttributes = currentAttributes & ~attributesToRemove; - fsInfo.Attributes = newAttributes; - fsInfo.Refresh(); - } + var currentAttributes = fsItem.Attributes; + var newAttributes = currentAttributes & ~attributesToRemove; + fsItem.Attributes = newAttributes; + fsItem.Refresh(); + } - /// - /// Set attributes from a given filesystem entry. - /// - /// The target filesystem handle. - /// Attributes to add. - /// is . - public static void SetAttributes(this IFileSystemInfo fsInfo, FileAttributes attributesToAdd) - { - if (fsInfo == null) - throw new ArgumentNullException(nameof(fsInfo)); + /// + /// Set attributes from a given filesystem entry. + /// + /// Attributes to add. + /// is . + public void SetAttributes(FileAttributes attributesToAdd) + { + if (fsItem == null) + throw new ArgumentNullException(nameof(fsItem)); - var currentAttributes = fsInfo.Attributes; - fsInfo.Attributes = currentAttributes | attributesToAdd; - fsInfo.Refresh(); + var currentAttributes = fsItem.Attributes; + fsItem.Attributes = currentAttributes | attributesToAdd; + fsItem.Refresh(); + } } } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs b/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs index 2cd00d8..c648504 100644 --- a/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs @@ -19,64 +19,67 @@ namespace AnakinRaW.CommonUtilities.FileSystem.Windows; public static class WindowsPathExtensions { // Based on: https://stackoverflow.com/questions/1410127/c-sharp-test-if-user-has-write-access-to-a-folder - /// - /// Checks whether the current executing user that the requested rights on a given location. - /// /// The directory to check rights on. - /// The requested rights. - /// - /// If does not exists. - /// If the current system is not Windows. - /// is . - public static bool UserHasDirectoryAccessRights(this IDirectoryInfo directoryInfo, FileSystemRights accessRights) + extension(IDirectoryInfo directoryInfo) { - ThrowHelper.ThrowIfNotWindows(); - if (directoryInfo == null) - throw new ArgumentNullException(nameof(directoryInfo)); - bool isInRoleWithAccess; - try + /// + /// Checks whether the current executing user that the requested rights on a given location. + /// + /// The requested rights. + /// + /// If does not exists. + /// If the current system is not Windows. + /// is . + public bool UserHasDirectoryAccessRights(FileSystemRights accessRights) { - if (!directoryInfo.Exists) - throw new DirectoryNotFoundException($"Unable to find {directoryInfo.FullName}"); - isInRoleWithAccess = TestAccessRightsOnWindows(directoryInfo, accessRights); - } - catch (UnauthorizedAccessException) - { - return false; - } - - return isInRoleWithAccess; - } + ThrowHelper.ThrowIfNotWindows(); + if (directoryInfo == null) + throw new ArgumentNullException(nameof(directoryInfo)); + bool isInRoleWithAccess; + try + { + if (!directoryInfo.Exists) + throw new DirectoryNotFoundException($"Unable to find {directoryInfo.FullName}"); + isInRoleWithAccess = TestAccessRightsOnWindows(directoryInfo, accessRights); + } + catch (UnauthorizedAccessException) + { + return false; + } - private static bool TestAccessRightsOnWindows(this IDirectoryInfo directoryInfo, FileSystemRights accessRights) - { - var acl = directoryInfo.GetAccessControl(); - var rules = acl.GetAccessRules(true, true, - // If Windows 7 - Environment.OSVersion.VersionString.StartsWith("6.1") - ? typeof(SecurityIdentifier) - : typeof(NTAccount)); + return isInRoleWithAccess; + } - var currentUser = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(currentUser); - foreach (AuthorizationRule rule in rules) + private bool TestAccessRightsOnWindows(FileSystemRights accessRights) { - if (rule is not FileSystemAccessRule fsAccessRule) - continue; + var acl = directoryInfo.GetAccessControl(); + var rules = acl.GetAccessRules(true, true, + // If Windows 7 + Environment.OSVersion.VersionString.StartsWith("6.1") + ? typeof(SecurityIdentifier) + : typeof(NTAccount)); - if ((fsAccessRule.FileSystemRights & accessRights) > 0) + var currentUser = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(currentUser); + foreach (AuthorizationRule rule in rules) { - var ntAccount = rule.IdentityReference as NTAccount; - if (ntAccount == null) + if (rule is not FileSystemAccessRule fsAccessRule) continue; - if (principal.IsInRole(ntAccount.Value)) + if ((fsAccessRule.FileSystemRights & accessRights) > 0) { - return fsAccessRule.AccessControlType != AccessControlType.Deny; + var ntAccount = rule.IdentityReference as NTAccount; + if (ntAccount == null) + continue; + + if (principal.IsInRole(ntAccount.Value)) + { + return fsAccessRule.AccessControlType != AccessControlType.Deny; + } } } - } - return false; + return false; + } } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Extensions.cs b/src/CommonUtilities.SimplePipeline/src/Extensions.cs index 49a4769..fc6bdd4 100644 --- a/src/CommonUtilities.SimplePipeline/src/Extensions.cs +++ b/src/CommonUtilities.SimplePipeline/src/Extensions.cs @@ -5,14 +5,17 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline; internal static class Extensions { - public static bool IsExceptionType(this Exception error) where T : Exception + extension(Exception error) { - return error switch + public bool IsExceptionType() where T : Exception { - T _ => true, - AggregateException aggregateException => aggregateException.InnerExceptions.Any(p => - p.IsExceptionType()), - _ => false - }; + return error switch + { + T _ => true, + AggregateException aggregateException => aggregateException.InnerExceptions.Any(p => + p.IsExceptionType()), + _ => false + }; + } } } \ No newline at end of file diff --git a/src/CommonUtilities/src/Extensions/EncodingExtensions.NetFramework.cs b/src/CommonUtilities/src/Extensions/EncodingExtensions.NetFramework.cs index a86af2a..70b1273 100644 --- a/src/CommonUtilities/src/Extensions/EncodingExtensions.NetFramework.cs +++ b/src/CommonUtilities/src/Extensions/EncodingExtensions.NetFramework.cs @@ -8,56 +8,56 @@ namespace AnakinRaW.CommonUtilities.Extensions; public static partial class EncodingExtensions { - /// - /// Encodes into a span of bytes a set of characters from the specified read-only span. - /// /// The encoding to use. - /// The span containing the set of characters to encode. - /// The byte span to hold the encoded bytes. - /// The number of encoded bytes. - public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan value, Span destination) + extension(Encoding encoding) { - fixed (char* charsPtr = &GetNonNullPinnableReference(value)) - fixed (byte* bytesPtr = &GetNonNullPinnableReference(destination)) - return encoding.GetBytes(charsPtr, value.Length, bytesPtr, destination.Length); - } + /// + /// Encodes into a span of bytes a set of characters from the specified read-only span. + /// + /// The span containing the set of characters to encode. + /// The byte span to hold the encoded bytes. + /// The number of encoded bytes. + public unsafe int GetBytes(ReadOnlySpan value, Span destination) + { + fixed (char* charsPtr = &GetNonNullPinnableReference(value)) + fixed (byte* bytesPtr = &GetNonNullPinnableReference(destination)) + return encoding.GetBytes(charsPtr, value.Length, bytesPtr, destination.Length); + } - /// - /// Decodes all the bytes in the specified read-only byte span into a character span. - /// - /// The encoding to use. - /// A read-only span containing the sequence of bytes to decode. - /// The character span receiving the decoded bytes. - /// The actual number of characters written at the span indicated by the parameter. - public static unsafe int GetChars(this Encoding encoding, ReadOnlySpan bytes, Span chars) - { - fixed (byte* pBytes = &GetNonNullPinnableReference(bytes)) - fixed (char* pChar = &GetNonNullPinnableReference(chars)) - return encoding.GetChars(pBytes, bytes.Length, pChar, chars.Length); - } + /// + /// Decodes all the bytes in the specified read-only byte span into a character span. + /// + /// A read-only span containing the sequence of bytes to decode. + /// The character span receiving the decoded bytes. + /// The actual number of characters written at the span indicated by the parameter. + public unsafe int GetChars(ReadOnlySpan bytes, Span chars) + { + fixed (byte* pBytes = &GetNonNullPinnableReference(bytes)) + fixed (char* pChar = &GetNonNullPinnableReference(chars)) + return encoding.GetChars(pBytes, bytes.Length, pChar, chars.Length); + } - /// - /// Decodes all the bytes in the specified byte span into a string. - /// - /// The encoding to use - /// A read-only byte span to decode to a Unicode string. - /// A string that contains the decoded bytes from the provided read-only span. - public static unsafe string GetString(this Encoding encoding, ReadOnlySpan bytes) - { - fixed (byte* bytesPtr = &GetNonNullPinnableReference(bytes)) - return encoding.GetString(bytesPtr, bytes.Length); - } + /// + /// Decodes all the bytes in the specified byte span into a string. + /// + /// A read-only byte span to decode to a Unicode string. + /// A string that contains the decoded bytes from the provided read-only span. + public unsafe string GetString(ReadOnlySpan bytes) + { + fixed (byte* bytesPtr = &GetNonNullPinnableReference(bytes)) + return encoding.GetString(bytesPtr, bytes.Length); + } - /// - /// Calculates the number of bytes produced by encoding the characters in the specified character span. - /// - /// The encoding to use. - /// The span of characters to encode. - /// The number of bytes produced by encoding the specified character span. - public static unsafe int GetByteCount(this Encoding encoding, ReadOnlySpan value) - { - fixed (char* charsPtr = &GetNonNullPinnableReference(value)) - return encoding.GetByteCount(charsPtr, value.Length); + /// + /// Calculates the number of bytes produced by encoding the characters in the specified character span. + /// + /// The span of characters to encode. + /// The number of bytes produced by encoding the specified character span. + public unsafe int GetByteCount(ReadOnlySpan value) + { + fixed (char* charsPtr = &GetNonNullPinnableReference(value)) + return encoding.GetByteCount(charsPtr, value.Length); + } } diff --git a/src/CommonUtilities/src/Extensions/EncodingExtensions.cs b/src/CommonUtilities/src/Extensions/EncodingExtensions.cs index a017ab6..f1b3da5 100644 --- a/src/CommonUtilities/src/Extensions/EncodingExtensions.cs +++ b/src/CommonUtilities/src/Extensions/EncodingExtensions.cs @@ -8,128 +8,125 @@ namespace AnakinRaW.CommonUtilities.Extensions; /// public static partial class EncodingExtensions { - /// - /// Encodes a string value. - /// - /// The string to encode. /// The encoding to use. - /// The encoded string. - /// or is . - public static string EncodeString(this Encoding encoding, string value) + extension(Encoding encoding) { - if (value == null) - throw new ArgumentNullException(nameof(value)); - return encoding.EncodeString(value.AsSpan()); - } + /// + /// Encodes a string value. + /// + /// The string to encode. + /// The encoded string. + /// or is . + public string EncodeString(string value) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + return encoding.EncodeString(value.AsSpan()); + } - /// - /// Encodes a string value. - /// - /// The string to encode. - /// Maximum bytes required for encoding. - /// The encoding to use. - /// The encoded string. - /// or is . - /// is less than actually required. - /// is negative. - public static string EncodeString(this Encoding encoding, string value, int maxByteCount) - { - if (value == null) - throw new ArgumentNullException(nameof(value)); + /// + /// Encodes a string value. + /// + /// The string to encode. + /// Maximum bytes required for encoding. + /// The encoded string. + /// or is . + /// is less than actually required. + /// is negative. + public string EncodeString(string value, int maxByteCount) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); - return encoding.EncodeString(value.AsSpan(), maxByteCount); - } + return encoding.EncodeString(value.AsSpan(), maxByteCount); + } - /// - /// Encodes a character sequence. - /// - /// The span of characters to encode. - /// The encoding to use. - /// The encoded string. - /// is . - public static string EncodeString(this Encoding encoding, ReadOnlySpan value) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); + /// + /// Encodes a character sequence. + /// + /// The span of characters to encode. + /// The encoded string. + /// is . + public string EncodeString(ReadOnlySpan value) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); - return encoding.EncodeString(value, encoding.GetMaxByteCount(value.Length)); - } + return encoding.EncodeString(value, encoding.GetMaxByteCount(value.Length)); + } - /// - /// Encodes a character sequence. - /// - /// The span of characters to encode. - /// Maximum bytes required for encoding. - /// The encoding to use. - /// The encoded string. - /// is . - /// is less than actually required. - /// is negative. - public static unsafe string EncodeString(this Encoding encoding, ReadOnlySpan value, int maxByteCount) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (maxByteCount < 0) - throw new ArgumentOutOfRangeException(nameof(maxByteCount), "value must not be negative."); + /// + /// Encodes a character sequence. + /// + /// The span of characters to encode. + /// Maximum bytes required for encoding. + /// The encoded string. + /// is . + /// is less than actually required. + /// is negative. + public unsafe string EncodeString(ReadOnlySpan value, int maxByteCount) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (maxByteCount < 0) + throw new ArgumentOutOfRangeException(nameof(maxByteCount), "value must not be negative."); - var buffer = maxByteCount <= 256 ? stackalloc byte[maxByteCount] : new byte[maxByteCount]; + var buffer = maxByteCount <= 256 ? stackalloc byte[maxByteCount] : new byte[maxByteCount]; - var stringBytes = encoding.GetBytesReadOnly(value, buffer); - return encoding.GetString(stringBytes); - } + var stringBytes = encoding.GetBytesReadOnly(value, buffer); + return encoding.GetString(stringBytes); + } - /// - /// Encodes into a span of characters a set of characters from the specified read-only span. - /// - /// The encoding to use. - /// The span of characters to encode. - /// The character span to hold the encoded characters. - /// The actual number of characters written at the span indicated by the parameter. - public static int EncodeString(this Encoding encoding, ReadOnlySpan source, Span destination) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - var numMaxBytes = encoding.GetMaxByteCount(source.Length); - return EncodeString(encoding, source, destination, numMaxBytes); - } + /// + /// Encodes into a span of characters a set of characters from the specified read-only span. + /// + /// The span of characters to encode. + /// The character span to hold the encoded characters. + /// The actual number of characters written at the span indicated by the parameter. + public int EncodeString(ReadOnlySpan source, Span destination) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + var numMaxBytes = encoding.GetMaxByteCount(source.Length); + return EncodeString(encoding, source, destination, numMaxBytes); + } - /// - /// Encodes into a span of characters a set of characters from the specified read-only span. - /// - /// The encoding to use. - /// The span of characters to encode. - /// The character span to hold the encoded characters. - /// Maximum bytes *not characters!* required for encoding. - /// The actual number of characters written at the span indicated by the parameter. - /// is . - /// is less than actually required. - /// is negative. - public static unsafe int EncodeString(this Encoding encoding, ReadOnlySpan source, Span destination, int maxByteCount) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (maxByteCount < 0) - throw new ArgumentOutOfRangeException(nameof(maxByteCount), "value must not be negative."); + /// + /// Encodes into a span of characters a set of characters from the specified read-only span. + /// + /// The span of characters to encode. + /// The character span to hold the encoded characters. + /// Maximum bytes *not characters!* required for encoding. + /// The actual number of characters written at the span indicated by the parameter. + /// is . + /// is less than actually required. + /// is negative. + public unsafe int EncodeString(ReadOnlySpan source, Span destination, int maxByteCount) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (maxByteCount < 0) + throw new ArgumentOutOfRangeException(nameof(maxByteCount), "value must not be negative."); - var buffer = maxByteCount > 265 ? new byte[maxByteCount] : stackalloc byte[maxByteCount]; - var bytes = encoding.GetBytesReadOnly(source, buffer); - return encoding.GetChars(bytes, destination); - } + var buffer = maxByteCount > 265 ? new byte[maxByteCount] : stackalloc byte[maxByteCount]; + var bytes = encoding.GetBytesReadOnly(source, buffer); + return encoding.GetChars(bytes, destination); + } - /// - /// Encodes into a read-only span of bytes a set of characters from the specified read-only span. - /// - /// - /// The returned read-only span is sliced from . - /// This means, modifying might also modify the returned read-only span. - /// - /// The encoding to use. - /// The span of characters to encode. - /// The byte span to hold the encoded bytes. - /// The read-only byte span that holds the encoded bytes. - public static ReadOnlySpan GetBytesReadOnly(this Encoding encoding, ReadOnlySpan value, Span inputBuffer) - { - var pathBytesWritten = encoding.GetBytes(value, inputBuffer); - return inputBuffer.Slice(0, pathBytesWritten); + /// + /// Encodes into a read-only span of bytes a set of characters from the specified read-only span. + /// + /// + /// The returned read-only span is sliced from . + /// This means, modifying might also modify the returned read-only span. + /// + /// The span of characters to encode. + /// The byte span to hold the encoded bytes. + /// The read-only byte span that holds the encoded bytes. + public ReadOnlySpan GetBytesReadOnly(ReadOnlySpan value, Span inputBuffer) + { + var pathBytesWritten = encoding.GetBytes(value, inputBuffer); + return inputBuffer.Slice(0, pathBytesWritten); + } } } \ No newline at end of file From f8144bf3addd686e9b697a38a6d8c19dda83afe3 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 7 Dec 2025 18:39:25 +0100 Subject: [PATCH 20/43] logging --- .../src/DownloadManager.cs | 10 +- .../src/Providers/HttpClientDownloader.cs | 6 +- .../src/Providers/WebClientDownloader.cs | 12 +- .../src/Runners/StepRunnerBase.cs | 4 +- .../src/Steps/PipelineStep.cs | 4 +- .../src/Steps/RunPipelineStep.cs | 4 +- .../src/Extensions/EncodingExtensions.cs | 217 +++++++++--------- 7 files changed, 129 insertions(+), 128 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/DownloadManager.cs b/src/CommonUtilities.DownloadManager/src/DownloadManager.cs index 14430df..55f7b15 100644 --- a/src/CommonUtilities.DownloadManager/src/DownloadManager.cs +++ b/src/CommonUtilities.DownloadManager/src/DownloadManager.cs @@ -90,7 +90,7 @@ public Task DownloadAsync( if (!uri.IsAbsoluteUri) throw new ArgumentException("Uri must be absolute.", nameof(uri)); - _logger?.LogTrace($"Download requested: {uri.AbsoluteUri}"); + _logger?.LogTrace("Download requested: {Uri}", uri.AbsoluteUri); if (uri is { IsFile: false, IsUnc: false }) { @@ -139,7 +139,7 @@ private async Task DownloadWithRetry( var length = outputStream.Length; try { - _logger?.LogTrace($"Attempting download '{uri.AbsoluteUri}' using provider '{provider.Name}'"); + _logger?.LogTrace("Attempting download '{Uri}' using provider '{ProviderName}'", uri.AbsoluteUri, provider.Name); var summary = await provider.DownloadAsync(uri, outputStream, status => { @@ -193,7 +193,7 @@ private async Task DownloadWithRetry( } } - _logger?.LogInformation($"Download of '{uri.AbsoluteUri}' succeeded using provider '{provider.Name}'"); + _logger?.LogInformation("Download of '{Uri}' succeeded using provider '{ProviderName}'", uri.AbsoluteUri, provider.Name); _leastRecentlyUsedDownloadProviders.LastSuccessfulProvider = provider.Name; summary.DownloadProvider = provider.Name; @@ -207,7 +207,7 @@ private async Task DownloadWithRetry( catch (Exception ex) { failureList.Add(new DownloadFailureInformation(ex, provider.Name)); - _logger?.LogTrace($"Download failed using {provider.Name} provider. {ex}"); + _logger?.LogTrace("Download failed using {Provider} provider. {Exception}", provider.Name, ex); if (provider.Equals(providers.LastOrDefault())) throw new DownloadFailedException(failureList); @@ -222,7 +222,7 @@ private async Task DownloadWithRetry( if (millisecondsTimeout <= 0) continue; - _logger?.LogTrace($"Sleeping {millisecondsTimeout} before retrying download."); + _logger?.LogTrace("Sleeping {WaitTime} before retrying download.", millisecondsTimeout); await Task.Delay(TimeSpan.FromMilliseconds(millisecondsTimeout), cancellationToken); } diff --git a/src/CommonUtilities.DownloadManager/src/Providers/HttpClientDownloader.cs b/src/CommonUtilities.DownloadManager/src/Providers/HttpClientDownloader.cs index 581a963..fc2609d 100644 --- a/src/CommonUtilities.DownloadManager/src/Providers/HttpClientDownloader.cs +++ b/src/CommonUtilities.DownloadManager/src/Providers/HttpClientDownloader.cs @@ -143,15 +143,15 @@ private static HttpRequestMessage CreateRequest(Uri uri) if (cancellationToken.IsCancellationRequested) { _logger?.LogTrace( - "HttpClient error with '" + uri.AbsoluteUri + "' - " + errorMessage); + "HttpClient error with '{Uri}' - {Message}", uri.AbsoluteUri, errorMessage); cancellationToken.ThrowIfCancellationRequested(); } - _logger?.LogTrace("WebClient error - '" + uri.AbsoluteUri + "'."); + _logger?.LogTrace("WebClient error - '{Uri}'.", uri.AbsoluteUri); throw; } catch (Exception ex) { - _logger?.LogError(ex, "General exception error in HttpClient"); + _logger?.LogError(ex, "General exception error in HttpClient: {Message}", ex.Message); throw; } finally diff --git a/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs b/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs index acfea4c..4031c19 100644 --- a/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs +++ b/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs @@ -91,13 +91,12 @@ protected override async Task DownloadAsyncCore( : "DownloadCore failed"; if (cancellationToken.IsCancellationRequested) { - _logger?.LogTrace("WebClient error '" + ex.Status + "' with '" + uri.AbsoluteUri + "' - " + - message); + _logger?.LogTrace("WebClient error '{Status}' with '{Uri}' - {Message}", ex.Status, uri.AbsoluteUri, message); cancellationToken.ThrowIfCancellationRequested(); } else { - _logger?.LogTrace("WebClient error '" + ex.Status + "' with '" + uri.AbsoluteUri + "'."); + _logger?.LogTrace("WebClient error '{Status}' with '{Uri}'.", ex.Status, uri.AbsoluteUri); throw; } } @@ -160,7 +159,7 @@ private static HttpWebRequest CreateRequest(Uri uri, DownloadOptions? downloadOp success = true; return httpWebResponse; default: - _logger?.LogTrace($"WebResponse error for '{uri.AbsoluteUri}' ({httpWebResponse.StatusCode})."); + _logger?.LogTrace("WebResponse error for '{Uri}' ({Status}).", uri.AbsoluteUri, httpWebResponse.StatusCode); break; } } @@ -171,12 +170,11 @@ private static HttpWebRequest CreateRequest(Uri uri, DownloadOptions? downloadOp : "GetWebResponse failed"; if (cancellationToken.IsCancellationRequested) { - _logger?.LogTrace( - "WebClient error '" + ex.Status + "' with '" + uri.AbsoluteUri + "' - " + errorMessage); + _logger?.LogTrace("WebClient error '{Status}' with '{Uri}' - {Message}", ex.Status, uri.AbsoluteUri, errorMessage); cancellationToken.ThrowIfCancellationRequested(); } - _logger?.LogTrace("WebClient error '" + ex.Status + "' - '" + uri.AbsoluteUri + "'."); + _logger?.LogTrace("WebClient error '{Status}' - '{Uri}'.", ex.Status, uri.AbsoluteUri); throw; } catch (Exception ex) diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs b/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs index d692c9e..2db7260 100644 --- a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs +++ b/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs @@ -87,9 +87,9 @@ protected void RunSteps(CancellationToken token) if (!alreadyCancelled) { if (e.IsExceptionType()) - Logger?.LogTrace($"Step {step} cancelled"); + Logger?.LogTrace("Step {Step} cancelled", step); else - Logger?.LogTrace(e, $"Step {step} threw an exception: {e.GetType()}: {e.Message}"); + Logger?.LogTrace(e, "Step {Step} threw an exception: {Exception}: {EMessage}", step, e.GetType(), e.Message); } var error = new StepRunnerErrorEventArgs(e, step) diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs index e15d119..94f4c41 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs @@ -40,11 +40,11 @@ protected PipelineStep(IServiceProvider serviceProvider) /// public void Run(CancellationToken token) { - Logger?.LogTrace($"BEGIN: {this}"); + Logger?.LogTrace("BEGIN: {Step}", this); try { RunCore(token); - Logger?.LogTrace($"END: {this}"); + Logger?.LogTrace("END: {Step}", this); } catch (OperationCanceledException ex) { diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs index acaac39..7a13f29 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs @@ -17,12 +17,12 @@ public class RunPipelineStep(IPipeline pipeline, IServiceProvider serviceProvide /// protected override void RunSynchronized(CancellationToken token) { - Logger?.LogTrace($"Running {_pipeline}..."); + Logger?.LogTrace("Running {Pipeline}...", _pipeline); try { // ReSharper disable once MethodSupportsCancellation _pipeline.RunAsync(token).Wait(); - Logger?.LogTrace($"Finished {_pipeline}"); + Logger?.LogTrace("Finished {Pipeline}", _pipeline); } catch (AggregateException e) { diff --git a/src/CommonUtilities/src/Extensions/EncodingExtensions.cs b/src/CommonUtilities/src/Extensions/EncodingExtensions.cs index f1b3da5..204917e 100644 --- a/src/CommonUtilities/src/Extensions/EncodingExtensions.cs +++ b/src/CommonUtilities/src/Extensions/EncodingExtensions.cs @@ -8,125 +8,128 @@ namespace AnakinRaW.CommonUtilities.Extensions; /// public static partial class EncodingExtensions { + /// + /// Encodes a string value. + /// + /// The string to encode. /// The encoding to use. - extension(Encoding encoding) + /// The encoded string. + /// or is . + public static string EncodeString(this Encoding encoding, string value) { - /// - /// Encodes a string value. - /// - /// The string to encode. - /// The encoded string. - /// or is . - public string EncodeString(string value) - { - if (value == null) - throw new ArgumentNullException(nameof(value)); - return encoding.EncodeString(value.AsSpan()); - } + if (value == null) + throw new ArgumentNullException(nameof(value)); + return encoding.EncodeString(value.AsSpan()); + } - /// - /// Encodes a string value. - /// - /// The string to encode. - /// Maximum bytes required for encoding. - /// The encoded string. - /// or is . - /// is less than actually required. - /// is negative. - public string EncodeString(string value, int maxByteCount) - { - if (value == null) - throw new ArgumentNullException(nameof(value)); + /// + /// Encodes a string value. + /// + /// The string to encode. + /// Maximum bytes required for encoding. + /// The encoding to use. + /// The encoded string. + /// or is . + /// is less than actually required. + /// is negative. + public static string EncodeString(this Encoding encoding, string value, int maxByteCount) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); - return encoding.EncodeString(value.AsSpan(), maxByteCount); - } + return encoding.EncodeString(value.AsSpan(), maxByteCount); + } - /// - /// Encodes a character sequence. - /// - /// The span of characters to encode. - /// The encoded string. - /// is . - public string EncodeString(ReadOnlySpan value) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); + /// + /// Encodes a character sequence. + /// + /// The span of characters to encode. + /// The encoding to use. + /// The encoded string. + /// is . + public static string EncodeString(this Encoding encoding, ReadOnlySpan value) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); - return encoding.EncodeString(value, encoding.GetMaxByteCount(value.Length)); - } + return encoding.EncodeString(value, encoding.GetMaxByteCount(value.Length)); + } - /// - /// Encodes a character sequence. - /// - /// The span of characters to encode. - /// Maximum bytes required for encoding. - /// The encoded string. - /// is . - /// is less than actually required. - /// is negative. - public unsafe string EncodeString(ReadOnlySpan value, int maxByteCount) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (maxByteCount < 0) - throw new ArgumentOutOfRangeException(nameof(maxByteCount), "value must not be negative."); + /// + /// Encodes a character sequence. + /// + /// The span of characters to encode. + /// Maximum bytes required for encoding. + /// The encoding to use. + /// The encoded string. + /// is . + /// is less than actually required. + /// is negative. + public static unsafe string EncodeString(this Encoding encoding, ReadOnlySpan value, int maxByteCount) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (maxByteCount < 0) + throw new ArgumentOutOfRangeException(nameof(maxByteCount), "value must not be negative."); - var buffer = maxByteCount <= 256 ? stackalloc byte[maxByteCount] : new byte[maxByteCount]; + var buffer = maxByteCount <= 256 ? stackalloc byte[maxByteCount] : new byte[maxByteCount]; - var stringBytes = encoding.GetBytesReadOnly(value, buffer); - return encoding.GetString(stringBytes); - } + var stringBytes = encoding.GetBytesReadOnly(value, buffer); + return encoding.GetString(stringBytes); + } - /// - /// Encodes into a span of characters a set of characters from the specified read-only span. - /// - /// The span of characters to encode. - /// The character span to hold the encoded characters. - /// The actual number of characters written at the span indicated by the parameter. - public int EncodeString(ReadOnlySpan source, Span destination) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - var numMaxBytes = encoding.GetMaxByteCount(source.Length); - return EncodeString(encoding, source, destination, numMaxBytes); - } + /// + /// Encodes into a span of characters a set of characters from the specified read-only span. + /// + /// The span of characters to encode. + /// The character span to hold the encoded characters. + /// The encoding to use. + /// The actual number of characters written at the span indicated by the parameter. + public static int EncodeString(this Encoding encoding, ReadOnlySpan source, Span destination) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + var numMaxBytes = encoding.GetMaxByteCount(source.Length); + return EncodeString(encoding, source, destination, numMaxBytes); + } - /// - /// Encodes into a span of characters a set of characters from the specified read-only span. - /// - /// The span of characters to encode. - /// The character span to hold the encoded characters. - /// Maximum bytes *not characters!* required for encoding. - /// The actual number of characters written at the span indicated by the parameter. - /// is . - /// is less than actually required. - /// is negative. - public unsafe int EncodeString(ReadOnlySpan source, Span destination, int maxByteCount) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (maxByteCount < 0) - throw new ArgumentOutOfRangeException(nameof(maxByteCount), "value must not be negative."); + /// + /// Encodes into a span of characters a set of characters from the specified read-only span. + /// + /// The span of characters to encode. + /// The character span to hold the encoded characters. + /// Maximum bytes *not characters!* required for encoding. + /// The encoding to use. + /// The actual number of characters written at the span indicated by the parameter. + /// is . + /// is less than actually required. + /// is negative. + public static unsafe int EncodeString(this Encoding encoding, ReadOnlySpan source, Span destination, int maxByteCount) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (maxByteCount < 0) + throw new ArgumentOutOfRangeException(nameof(maxByteCount), "value must not be negative."); - var buffer = maxByteCount > 265 ? new byte[maxByteCount] : stackalloc byte[maxByteCount]; - var bytes = encoding.GetBytesReadOnly(source, buffer); - return encoding.GetChars(bytes, destination); - } + var buffer = maxByteCount > 265 ? new byte[maxByteCount] : stackalloc byte[maxByteCount]; + var bytes = encoding.GetBytesReadOnly(source, buffer); + return encoding.GetChars(bytes, destination); + } - /// - /// Encodes into a read-only span of bytes a set of characters from the specified read-only span. - /// - /// - /// The returned read-only span is sliced from . - /// This means, modifying might also modify the returned read-only span. - /// - /// The span of characters to encode. - /// The byte span to hold the encoded bytes. - /// The read-only byte span that holds the encoded bytes. - public ReadOnlySpan GetBytesReadOnly(ReadOnlySpan value, Span inputBuffer) - { - var pathBytesWritten = encoding.GetBytes(value, inputBuffer); - return inputBuffer.Slice(0, pathBytesWritten); - } + /// + /// Encodes into a read-only span of bytes a set of characters from the specified read-only span. + /// + /// + /// The returned read-only span is sliced from . + /// This means, modifying might also modify the returned read-only span. + /// + /// The span of characters to encode. + /// The byte span to hold the encoded bytes. + /// The encoding to use. + /// The read-only byte span that holds the encoded bytes. + public static ReadOnlySpan GetBytesReadOnly(this Encoding encoding, ReadOnlySpan value, Span inputBuffer) + { + var pathBytesWritten = encoding.GetBytes(value, inputBuffer); + return inputBuffer.Slice(0, pathBytesWritten); } } \ No newline at end of file From a8a56c5fdff700880a90c8fb09d6749f8ec5630e Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 7 Dec 2025 18:46:05 +0100 Subject: [PATCH 21/43] revert extensions --- .../src/Extensions/FileSystemExtensions.cs | 94 +++++++++---------- .../Extensions/FileSystemInfoExtensions.cs | 79 ++++++++-------- .../src/Windows/WindowsPathExtensions.cs | 93 +++++++++--------- 3 files changed, 130 insertions(+), 136 deletions(-) diff --git a/src/CommonUtilities.FileSystem/src/Extensions/FileSystemExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/FileSystemExtensions.cs index 289843e..e972643 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/FileSystemExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/FileSystemExtensions.cs @@ -9,58 +9,56 @@ namespace AnakinRaW.CommonUtilities.FileSystem; /// public static class FileSystemExtensions { + /// + /// Tries to create a new file and returns an open to the created file, or if the file could not be created. + /// An existing file will be overwritten. + /// + /// The file's location. + /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. + /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. + /// Number of retry attempts tempts until the operation fails. + /// Delay time in ms between each new attempt. /// - extension(IFileSystem fs) + /// Open file stream or if the file could not be created. + /// or is . + public static FileSystemStream? CreateFileWithRetry(this IFileSystem fs, string path, + FileAccess fileAccess = FileAccess.ReadWrite, + FileShare fileShare = FileShare.None, + int retryCount = 2, + int retryDelay = 500) { - /// - /// Tries to create a new file and returns an open to the created file, or if the file could not be created. - /// An existing file will be overwritten. - /// - /// The file's location. - /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. - /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. - /// Number of retry attempts tempts until the operation fails. - /// Delay time in ms between each new attempt. - /// Open file stream or if the file could not be created. - /// or is . - public FileSystemStream? CreateFileWithRetry(string path, - FileAccess fileAccess = FileAccess.ReadWrite, - FileShare fileShare = FileShare.None, - int retryCount = 2, - int retryDelay = 500) - { - if (fs == null) - throw new ArgumentNullException(nameof(fs)); - if (path == null) - throw new ArgumentNullException(nameof(path)); + if (fs == null) + throw new ArgumentNullException(nameof(fs)); + if (path == null) + throw new ArgumentNullException(nameof(path)); - FileSystemStream? stream = null; - FileSystemUtilities.ExecuteFileSystemActionWithRetry(retryCount, retryDelay, - () => stream = fs.FileStream.New(path, FileMode.Create, fileAccess, fileShare)); - return stream; - } + FileSystemStream? stream = null; + FileSystemUtilities.ExecuteFileSystemActionWithRetry(retryCount, retryDelay, + () => stream = fs.FileStream.New(path, FileMode.Create, fileAccess, fileShare)); + return stream; + } - /// - /// Tries to create a new unique folder within the current users temporary directory. - /// - /// Number of retry attempts tempts until the operation fails. - /// Delay time in ms between each new attempt. - /// The of the created folder or . - /// is . - public IDirectoryInfo? CreateTemporaryFolderInTempWithRetry(int retryCount = 2, int retryDelay = 500) - { - if (fs == null) - throw new ArgumentNullException(nameof(fs)); + /// + /// Tries to create a new unique folder within the current users temporary directory. + /// + /// Number of retry attempts tempts until the operation fails. + /// Delay time in ms between each new attempt. + /// + /// The of the created folder or . + /// is . + public static IDirectoryInfo? CreateTemporaryFolderInTempWithRetry(this IFileSystem fs, int retryCount = 2, int retryDelay = 500) + { + if (fs == null) + throw new ArgumentNullException(nameof(fs)); - IDirectoryInfo? newTempFolder = null; - FileSystemUtilities.ExecuteFileSystemActionWithRetry(retryCount, retryDelay, () => - { - var tempFolder = fs.Path.GetTempPath(); - var folderName = fs.Path.GetRandomFileName(); - var fullFolderPath = fs.Path.Combine(tempFolder, folderName); - newTempFolder = fs.Directory.CreateDirectory(fullFolderPath); - }); - return newTempFolder; - } + IDirectoryInfo? newTempFolder = null; + FileSystemUtilities.ExecuteFileSystemActionWithRetry(retryCount, retryDelay, () => + { + var tempFolder = fs.Path.GetTempPath(); + var folderName = fs.Path.GetRandomFileName(); + var fullFolderPath = fs.Path.Combine(tempFolder, folderName); + newTempFolder = fs.Directory.CreateDirectory(fullFolderPath); + }); + return newTempFolder; } } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Extensions/FileSystemInfoExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/FileSystemInfoExtensions.cs index 448737e..49c24fa 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/FileSystemInfoExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/FileSystemInfoExtensions.cs @@ -9,52 +9,51 @@ namespace AnakinRaW.CommonUtilities.FileSystem; /// public static class FileSystemInfoExtensions { + /// + /// Gets the remaining free bytes on the drive where is located. + /// /// Some file or directory at the targeted drive. - extension(IFileSystemInfo fsItem) + /// free drive space in bytes + /// is . + public static long GetDriveFreeSpace(this IFileSystemInfo fsItem) { - /// - /// Gets the remaining free bytes on the drive where is located. - /// - /// free drive space in bytes - /// is . - public long GetDriveFreeSpace() - { - if (fsItem == null) - throw new ArgumentNullException(nameof(fsItem)); + if (fsItem == null) + throw new ArgumentNullException(nameof(fsItem)); - var root = fsItem.FileSystem.Path.GetPathRoot(fsItem.FullName); - return fsItem.FileSystem.DriveInfo.New(root!).AvailableFreeSpace; - } + var root = fsItem.FileSystem.Path.GetPathRoot(fsItem.FullName); + return fsItem.FileSystem.DriveInfo.New(root!).AvailableFreeSpace; + } - /// - /// Removes attributes from a given filesystem entry. - /// - /// Attributes to remove. - /// is . - public void RemoveAttributes(FileAttributes attributesToRemove) - { - if (fsItem == null) - throw new ArgumentNullException(nameof(fsItem)); + /// + /// Removes attributes from a given filesystem entry. + /// + /// Attributes to remove. + /// Some file or directory at the targeted drive. + /// is . + public static void RemoveAttributes(this IFileSystemInfo fsItem, FileAttributes attributesToRemove) + { + if (fsItem == null) + throw new ArgumentNullException(nameof(fsItem)); - var currentAttributes = fsItem.Attributes; - var newAttributes = currentAttributes & ~attributesToRemove; - fsItem.Attributes = newAttributes; - fsItem.Refresh(); - } + var currentAttributes = fsItem.Attributes; + var newAttributes = currentAttributes & ~attributesToRemove; + fsItem.Attributes = newAttributes; + fsItem.Refresh(); + } - /// - /// Set attributes from a given filesystem entry. - /// - /// Attributes to add. - /// is . - public void SetAttributes(FileAttributes attributesToAdd) - { - if (fsItem == null) - throw new ArgumentNullException(nameof(fsItem)); + /// + /// Set attributes from a given filesystem entry. + /// + /// Attributes to add. + /// Some file or directory at the targeted drive. + /// is . + public static void SetAttributes(this IFileSystemInfo fsItem, FileAttributes attributesToAdd) + { + if (fsItem == null) + throw new ArgumentNullException(nameof(fsItem)); - var currentAttributes = fsItem.Attributes; - fsItem.Attributes = currentAttributes | attributesToAdd; - fsItem.Refresh(); - } + var currentAttributes = fsItem.Attributes; + fsItem.Attributes = currentAttributes | attributesToAdd; + fsItem.Refresh(); } } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs b/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs index c648504..2cd00d8 100644 --- a/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs @@ -19,67 +19,64 @@ namespace AnakinRaW.CommonUtilities.FileSystem.Windows; public static class WindowsPathExtensions { // Based on: https://stackoverflow.com/questions/1410127/c-sharp-test-if-user-has-write-access-to-a-folder + /// + /// Checks whether the current executing user that the requested rights on a given location. + /// /// The directory to check rights on. - extension(IDirectoryInfo directoryInfo) + /// The requested rights. + /// + /// If does not exists. + /// If the current system is not Windows. + /// is . + public static bool UserHasDirectoryAccessRights(this IDirectoryInfo directoryInfo, FileSystemRights accessRights) { - /// - /// Checks whether the current executing user that the requested rights on a given location. - /// - /// The requested rights. - /// - /// If does not exists. - /// If the current system is not Windows. - /// is . - public bool UserHasDirectoryAccessRights(FileSystemRights accessRights) + ThrowHelper.ThrowIfNotWindows(); + if (directoryInfo == null) + throw new ArgumentNullException(nameof(directoryInfo)); + bool isInRoleWithAccess; + try { - ThrowHelper.ThrowIfNotWindows(); - if (directoryInfo == null) - throw new ArgumentNullException(nameof(directoryInfo)); - bool isInRoleWithAccess; - try - { - if (!directoryInfo.Exists) - throw new DirectoryNotFoundException($"Unable to find {directoryInfo.FullName}"); - isInRoleWithAccess = TestAccessRightsOnWindows(directoryInfo, accessRights); - } - catch (UnauthorizedAccessException) - { - return false; - } - - return isInRoleWithAccess; + if (!directoryInfo.Exists) + throw new DirectoryNotFoundException($"Unable to find {directoryInfo.FullName}"); + isInRoleWithAccess = TestAccessRightsOnWindows(directoryInfo, accessRights); + } + catch (UnauthorizedAccessException) + { + return false; } - private bool TestAccessRightsOnWindows(FileSystemRights accessRights) + return isInRoleWithAccess; + } + + private static bool TestAccessRightsOnWindows(this IDirectoryInfo directoryInfo, FileSystemRights accessRights) + { + var acl = directoryInfo.GetAccessControl(); + var rules = acl.GetAccessRules(true, true, + // If Windows 7 + Environment.OSVersion.VersionString.StartsWith("6.1") + ? typeof(SecurityIdentifier) + : typeof(NTAccount)); + + var currentUser = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(currentUser); + foreach (AuthorizationRule rule in rules) { - var acl = directoryInfo.GetAccessControl(); - var rules = acl.GetAccessRules(true, true, - // If Windows 7 - Environment.OSVersion.VersionString.StartsWith("6.1") - ? typeof(SecurityIdentifier) - : typeof(NTAccount)); + if (rule is not FileSystemAccessRule fsAccessRule) + continue; - var currentUser = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(currentUser); - foreach (AuthorizationRule rule in rules) + if ((fsAccessRule.FileSystemRights & accessRights) > 0) { - if (rule is not FileSystemAccessRule fsAccessRule) + var ntAccount = rule.IdentityReference as NTAccount; + if (ntAccount == null) continue; - if ((fsAccessRule.FileSystemRights & accessRights) > 0) + if (principal.IsInRole(ntAccount.Value)) { - var ntAccount = rule.IdentityReference as NTAccount; - if (ntAccount == null) - continue; - - if (principal.IsInRole(ntAccount.Value)) - { - return fsAccessRule.AccessControlType != AccessControlType.Deny; - } + return fsAccessRule.AccessControlType != AccessControlType.Deny; } } - - return false; } + + return false; } } \ No newline at end of file From ee205f7be7794f722e8f0516ad7e0bb383985624 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 8 Dec 2025 11:46:40 +0100 Subject: [PATCH 22/43] remove some warning --- .../test/Progress/ProgressTypeTest.cs | 4 ++-- .../Collections/IReadOnlyListTestSuite.cs | 2 ++ .../test/Collections/FrugalListTestBase.cs | 6 ++++++ .../test/Collections/ReadOnlyFrugalListTestBase.cs | 8 ++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/CommonUtilities.SimplePipeline/test/Progress/ProgressTypeTest.cs b/src/CommonUtilities.SimplePipeline/test/Progress/ProgressTypeTest.cs index 8ea839c..e559be4 100644 --- a/src/CommonUtilities.SimplePipeline/test/Progress/ProgressTypeTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Progress/ProgressTypeTest.cs @@ -57,10 +57,10 @@ public void EqualsGetHashCode() Assert.True(pt.Equals(pt)); Assert.True(pt.Equals((object)pt)); - // ReSharper disable once EqualExpressionComparison +#pragma warning disable CS1718 // ReSharper disable EqualExpressionComparison Assert.True(pt == pt); - // ReSharper disable once EqualExpressionComparison Assert.False(pt != pt); +#pragma warning restore CS1718 Assert.True(pt.Equals(equal)); Assert.True(pt == equal); Assert.False(pt != equal); diff --git a/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyListTestSuite.cs b/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyListTestSuite.cs index cc7cc52..fb0ef1b 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyListTestSuite.cs +++ b/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyListTestSuite.cs @@ -40,7 +40,9 @@ protected override IReadOnlyCollection GenericIReadOnlyCollectionFactory(IEnu [Theory] [MemberData(nameof(GetEnumerableTestData))] + #pragma warning disable xUnit1026 public void From_IEnumerable(int _, int enumerableLength, int __, int numberOfDuplicateElements) + #pragma warning restore xUnit1026 { var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); var list = GenericIReadOnlyListFactory(enumerable); diff --git a/src/CommonUtilities/test/Collections/FrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalListTestBase.cs index 551fdce..d9f60ea 100644 --- a/src/CommonUtilities/test/Collections/FrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalListTestBase.cs @@ -7,6 +7,8 @@ namespace AnakinRaW.CommonUtilities.Test.Collections; +#pragma warning disable xUnit2013 + /// /// Contains tests that ensure the correctness of the class. /// @@ -98,7 +100,9 @@ public void Constructor_IEnumerable(int _, int enumerableLength, int __, int num [Theory] [MemberData(nameof(GetEnumerableTestData))] + #pragma warning disable xUnit1026 public void Constructor_IEnumerable_Creates_Copy(int _, int enumerableLength, int __, int numberOfDuplicateElements) + #pragma warning restore xUnit1026 { foreach (var modifyEnumerable in GetModifyEnumerables(ModifyEnumeratorThrows)) { @@ -316,7 +320,9 @@ public void ToArray(int count) [Theory] [MemberData(nameof(GetEnumerableTestData))] + #pragma warning disable xUnit1026 public void GetEnumerator(int _, int enumerableLength, int __, int numberOfDuplicateElements) + #pragma warning restore xUnit1026 { var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); var list = new FrugalList(enumerable); diff --git a/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTestBase.cs b/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTestBase.cs index 1134afb..53313a0 100644 --- a/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTestBase.cs @@ -8,6 +8,8 @@ namespace AnakinRaW.CommonUtilities.Test.Collections; +#pragma warning disable xUnit2013 + /// /// Contains tests that ensure the correctness of the class. /// @@ -37,7 +39,9 @@ protected override IReadOnlyList GenericIReadOnlyListFactory(IEnumerable e [Fact] public void Empty_Idempotent() { + #pragma warning disable xUnit2002 Assert.NotNull(ReadOnlyFrugalList.Empty); + #pragma warning restore xUnit2002 Assert.Equal(0, ReadOnlyFrugalList.Empty.Count); Assert.Equal(ReadOnlyFrugalList.Empty, ReadOnlyFrugalList.Empty); } @@ -63,7 +67,9 @@ public void Ctor_Single() [Theory] [MemberData(nameof(GetEnumerableTestData))] + #pragma warning disable xUnit1026 public void Ctor_ModificationsGetNotReflectedWhenOriginalListChanges(int _, int enumerableLength, int __, int numberOfDuplicateElements) + #pragma warning restore xUnit1026 { var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); @@ -228,7 +234,9 @@ public void ToArray(int count) [Theory] [MemberData(nameof(GetEnumerableTestData))] + #pragma warning disable xUnit1026 public void GetEnumerator(int _, int enumerableLength, int __, int numberOfDuplicateElements) + #pragma warning restore xUnit1026 { var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); var list = new FrugalList(enumerable); From f9724fcf2ac2c77eaefa28e45fa966d52eb3d01e Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 8 Dec 2025 11:50:39 +0100 Subject: [PATCH 23/43] remove more warnings --- src/CommonUtilities/test/Collections/FrugalListTestBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CommonUtilities/test/Collections/FrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalListTestBase.cs index d9f60ea..c58981f 100644 --- a/src/CommonUtilities/test/Collections/FrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalListTestBase.cs @@ -84,7 +84,9 @@ public void Constructor_OtherFrugalList_Creates_Copy(int count) [Theory] [MemberData(nameof(GetEnumerableTestData))] + #pragma warning disable xUnit1026 public void Constructor_IEnumerable(int _, int enumerableLength, int __, int numberOfDuplicateElements) + #pragma warning restore xUnit1026 { var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); var list = new FrugalList(enumerable); From 56b024730142301eb6fc14f732cfd4514abfb35b Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 12 Dec 2025 13:12:28 +0100 Subject: [PATCH 24/43] update deps --- .../src/CommonUtilities.DownloadManager.csproj | 4 ++-- .../src/LeastRecentlyUsedDownloadProviders.cs | 2 +- .../src/Providers/HttpClientDownloader.cs | 4 ++-- .../src/Providers/WebClientDownloader.cs | 2 +- .../test/CommonUtilities.DownloadManager.Test.csproj | 2 +- .../test/StreamUtilitiesTest.cs | 3 ++- .../CompilerServices/CallerArgumentExpressionAttribute.cs | 2 ++ .../src/Extensions/PathExtensions.cs | 2 -- .../src/CommonUtilities.SimplePipeline.csproj | 4 ++-- src/CommonUtilities.SimplePipeline/src/Extensions.cs | 2 +- .../test/CommonUtilities.SimplePipeline.Test.csproj | 4 ++-- .../CommonUtilities.TestingUtilities.csproj | 6 +++--- src/CommonUtilities/src/CommonUtilities.csproj | 4 ++-- .../CompilerServices/CallerArgumentExpressionAttribute.cs | 2 ++ .../src/Hashing/Providers/HashAlgorithmProviderBase.cs | 2 +- src/CommonUtilities/test/CommonUtilities.Test.csproj | 4 ++-- 16 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index b3c74e5..8aa9724 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -30,8 +30,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs b/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs index 9af186b..3fc8d58 100644 --- a/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs +++ b/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs @@ -46,7 +46,7 @@ public IList GetProvidersInPriorityOrder(ICollection DownloadAsyncCore( { if (response is not null) { -#if NET - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); +#if NET || NETSTANDARD2_1_OR_GREATER + await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); #else using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); #endif diff --git a/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs b/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs index 4031c19..9bf6836 100644 --- a/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs +++ b/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs @@ -68,7 +68,7 @@ protected override async Task DownloadAsyncCore( try { summary.DownloadedSize = await StreamUtilities.CopyStreamWithProgressAsync( - responseStream, + responseStream!, totalStreamLength, outputStream, progress, diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 4ead415..6196ee1 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CommonUtilities.DownloadManager/test/StreamUtilitiesTest.cs b/src/CommonUtilities.DownloadManager/test/StreamUtilitiesTest.cs index febcb41..0c5bd1a 100644 --- a/src/CommonUtilities.DownloadManager/test/StreamUtilitiesTest.cs +++ b/src/CommonUtilities.DownloadManager/test/StreamUtilitiesTest.cs @@ -30,8 +30,9 @@ public async Task CopyStreamWithProgressAsync_StreamLengthAndCorrectCopy() Assert.Equal(3, bytesRead); output.Seek(0, SeekOrigin.Begin); var outputData = new byte[3]; - output.Read(outputData, 0, 3); + var n = output.Read(outputData, 0, 3); Assert.Equal(inputData, outputData); + Assert.Equal(3, n); } [Fact] diff --git a/src/CommonUtilities.FileSystem/src/CompilerServices/CallerArgumentExpressionAttribute.cs b/src/CommonUtilities.FileSystem/src/CompilerServices/CallerArgumentExpressionAttribute.cs index 4d6a90e..33eb37f 100644 --- a/src/CommonUtilities.FileSystem/src/CompilerServices/CallerArgumentExpressionAttribute.cs +++ b/src/CommonUtilities.FileSystem/src/CompilerServices/CallerArgumentExpressionAttribute.cs @@ -1,5 +1,7 @@ #if !NET +#pragma warning disable IDE0130 namespace System.Runtime.CompilerServices; +#pragma warning restore IDE0130 [AttributeUsage(AttributeTargets.Parameter)] internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs index bc0aba1..439261b 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs @@ -135,8 +135,6 @@ public static bool IsDriveRelative(this IPath fsPath, ReadOnlySpan path, [ driveLetter = path[0]; return true; } - - return false; } return false; } diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index 318ea7f..6b2d33d 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -25,8 +25,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/src/Extensions.cs b/src/CommonUtilities.SimplePipeline/src/Extensions.cs index fc6bdd4..12dbb89 100644 --- a/src/CommonUtilities.SimplePipeline/src/Extensions.cs +++ b/src/CommonUtilities.SimplePipeline/src/Extensions.cs @@ -11,7 +11,7 @@ public bool IsExceptionType() where T : Exception { return error switch { - T _ => true, + T => true, AggregateException aggregateException => aggregateException.InnerExceptions.Any(p => p.IsExceptionType()), _ => false diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index fd1fc46..4cef3d5 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -21,8 +21,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index f80f3b1..084c890 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -21,9 +21,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index b2951cd..f6180ae 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -26,8 +26,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/src/CommonUtilities/src/CompilerServices/CallerArgumentExpressionAttribute.cs b/src/CommonUtilities/src/CompilerServices/CallerArgumentExpressionAttribute.cs index 4d6a90e..33eb37f 100644 --- a/src/CommonUtilities/src/CompilerServices/CallerArgumentExpressionAttribute.cs +++ b/src/CommonUtilities/src/CompilerServices/CallerArgumentExpressionAttribute.cs @@ -1,5 +1,7 @@ #if !NET +#pragma warning disable IDE0130 namespace System.Runtime.CompilerServices; +#pragma warning restore IDE0130 [AttributeUsage(AttributeTargets.Parameter)] internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute diff --git a/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs b/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs index 47aa8a3..9d50323 100644 --- a/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs +++ b/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs @@ -75,7 +75,7 @@ protected async ValueTask ComputeHashAsyncWithHashAlgorithmLegacy(Stream so algorithm.TransformBlock(buffer, 0, bytesRead, buffer, 0); algorithm.TransformFinalBlock(buffer, 0, bytesRead); - var hashValue = algorithm.Hash!; + var hashValue = algorithm.Hash; algorithm.Hash.CopyTo(destination); return hashValue.Length; } diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index 3948201..39eb3bd 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -18,8 +18,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 36b456c388b553abf7c98e4f1a013887956a321f Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 12 Dec 2025 13:26:30 +0100 Subject: [PATCH 25/43] fix build --- .../src/Hashing/Providers/HashAlgorithmProviderBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs b/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs index 9d50323..47aa8a3 100644 --- a/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs +++ b/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs @@ -75,7 +75,7 @@ protected async ValueTask ComputeHashAsyncWithHashAlgorithmLegacy(Stream so algorithm.TransformBlock(buffer, 0, bytesRead, buffer, 0); algorithm.TransformFinalBlock(buffer, 0, bytesRead); - var hashValue = algorithm.Hash; + var hashValue = algorithm.Hash!; algorithm.Hash.CopyTo(destination); return hashValue.Length; } From 689848665894e25a2d3f17465089f018f3c4f947 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 12 Dec 2025 13:29:05 +0100 Subject: [PATCH 26/43] fix build --- .../src/LeastRecentlyUsedDownloadProviders.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs b/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs index 3fc8d58..9af186b 100644 --- a/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs +++ b/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs @@ -46,7 +46,7 @@ public IList GetProvidersInPriorityOrder(ICollection Date: Mon, 15 Dec 2025 17:02:48 +0100 Subject: [PATCH 27/43] Bump the actions-deps group with 2 updates (#290) Bumps the actions-deps group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/upload-artifact` from 5 to 6 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) Updates `actions/download-artifact` from 6 to 7 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f807c2e..f9e6004 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: - name: Create packages run: dotnet pack --configuration Release --output ./packages - name: Upload a Build Artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: NuGet packages path: packages/*.* @@ -45,7 +45,7 @@ jobs: fetch-depth: 0 - name: Setup .NET uses: actions/setup-dotnet@v5 - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: NuGet packages path: packages From 075729f9cfd895578bc5f0628eac074f296b6393 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 28 Dec 2025 10:59:04 +0100 Subject: [PATCH 28/43] Add ValueListDictionary (#291) * add and test value list * remove net 9 from pacakges and CI * update targets * clean up project files * fix nullability --- .github/workflows/test.yml | 1 - .../CommonUtilities.DownloadManager.csproj | 12 +- ...ommonUtilities.DownloadManager.Test.csproj | 1 + .../src/Commonutilities.FileSystem.csproj | 23 +- .../src/CommonUtilities.Registry.csproj | 9 +- .../src/Extensions/RegistryKeyExtensions.cs | 8 +- .../src/RegistryKeyBase.cs | 4 +- .../src/CommonUtilities.SimplePipeline.csproj | 9 +- .../ParallelProducerConsumerPipeline.cs | 4 +- .../src/Pipelines/StepRunnerPipeline.cs | 4 +- .../Progress/AggregatedProgressReporter.cs | 4 +- .../Collections/ICollectionTestSuite.cs | 2 +- .../Collections/IEnumerableTestSuite.cs | 812 +++++++++++++++++- .../INonModifyingEnumerableTestSuite.cs | 489 ----------- .../IReadOnlyCollectionTestSuite.cs | 7 +- .../CommonUtilities.TestingUtilities.csproj | 20 +- .../EqualityComparerConstantHashCode.cs | 8 + .../src/Collections/DebugViews.cs | 51 ++ .../src/Collections/FrugalList.cs | 3 + .../IReadOnlyValueListDictionary.cs | 146 ++++ .../src/Collections/IValueListDictionary.cs | 59 ++ .../src/Collections/ReadOnlyFrugalList.cs | 3 + .../ReadOnlyValueListDictionary.cs | 156 ++++ .../src/Collections/ValueListDictionary.cs | 630 ++++++++++++++ .../src/CommonUtilities.csproj | 11 +- .../test/Collections/DebugViewTest.cs | 318 +++++++ .../{ => FrugalList}/FrugalListTest.cs | 2 +- .../{ => FrugalList}/FrugalListTestBase.cs | 6 +- .../ReadOnlyFrugalListTestBase.cs | 96 ++- .../ReadOnlyFrugalListTests.cs | 33 +- .../IReadOnlyValueListDictionaryTestBase.cs | 743 ++++++++++++++++ .../IValueListDictionaryTestBase.cs | 430 ++++++++++ .../ReadOnlyValueListDictionaryTest.Keys.cs | 67 ++ .../ReadOnlyValueListDictionaryTest.Values.cs | 76 ++ .../ReadOnlyValueListDictionaryTestBase.cs | 94 ++ .../ReadOnlyValueListDictionaryTests.cs | 40 + .../ValueListDictionaryTest.Keys.cs | 67 ++ .../ValueListDictionaryTest.Values.cs | 76 ++ .../ValueListDictionaryTestBase.cs | 75 ++ .../ValueListDictionaryTests.cs | 37 + .../test/CommonUtilities.Test.csproj | 20 +- 41 files changed, 4098 insertions(+), 558 deletions(-) delete mode 100644 src/CommonUtilities.TestingUtilities/Collections/INonModifyingEnumerableTestSuite.cs create mode 100644 src/CommonUtilities.TestingUtilities/EqualityComparerConstantHashCode.cs create mode 100644 src/CommonUtilities/src/Collections/DebugViews.cs create mode 100644 src/CommonUtilities/src/Collections/IReadOnlyValueListDictionary.cs create mode 100644 src/CommonUtilities/src/Collections/IValueListDictionary.cs create mode 100644 src/CommonUtilities/src/Collections/ReadOnlyValueListDictionary.cs create mode 100644 src/CommonUtilities/src/Collections/ValueListDictionary.cs create mode 100644 src/CommonUtilities/test/Collections/DebugViewTest.cs rename src/CommonUtilities/test/Collections/{ => FrugalList}/FrugalListTest.cs (89%) rename src/CommonUtilities/test/Collections/{ => FrugalList}/FrugalListTestBase.cs (98%) rename src/CommonUtilities/test/Collections/{ => FrugalList}/ReadOnlyFrugalListTestBase.cs (74%) rename src/CommonUtilities/test/Collections/{ => FrugalList}/ReadOnlyFrugalListTests.cs (50%) create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Keys.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Values.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTests.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Keys.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Values.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestBase.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTests.cs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6b9298..233e4a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,6 @@ jobs: dotnet-version: | 6.0.x 8.0.x - 9.0.x 10.0.x - name: Build & Test in Release Mode diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index 8aa9724..7f6efce 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -1,11 +1,11 @@ - + Simple Download Manager supporting the local file system and HTTP downlaods. - netstandard2.0;netstandard2.1;net9.0 + netstandard2.0;netstandard2.1;net10.0 AnakinRaW.CommonUtilities.DownloadManager AnakinRaW.CommonUtilities.DownloadManager enable @@ -21,6 +21,11 @@ true + + + + + all @@ -30,9 +35,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 6196ee1..3861f54 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -17,6 +17,7 @@ + all diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj index 06387b7..53b6baf 100644 --- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj +++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj @@ -1,11 +1,11 @@ - + Helper classes and methods targeting the file system. - netstandard2.0;netstandard2.1;net9.0 + netstandard2.0;netstandard2.1;net10.0 AnakinRaW.CommonUtilities.FileSystem AnakinRaW.CommonUtilities.FileSystem enable @@ -22,18 +22,23 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj b/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj index bcd076d..a2ef07f 100644 --- a/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj +++ b/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj @@ -5,7 +5,7 @@ - netstandard2.0 + netstandard2.0;net10.0 AnakinRaW.CommonUtilities.Registry AnakinRaW.CommonUtilities.Registry enable @@ -20,13 +20,16 @@ true + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - diff --git a/src/CommonUtilities.Registry/src/Extensions/RegistryKeyExtensions.cs b/src/CommonUtilities.Registry/src/Extensions/RegistryKeyExtensions.cs index cd90ee8..fc2ec18 100644 --- a/src/CommonUtilities.Registry/src/Extensions/RegistryKeyExtensions.cs +++ b/src/CommonUtilities.Registry/src/Extensions/RegistryKeyExtensions.cs @@ -62,8 +62,8 @@ private static async Task WaitForRegistryKeyChangeAsync( try { - InMemoryRegistryKeyData.RegistryChanged += OnRegistryChanged; - inMemoryRegistryKey.Disposing += OnKeyDisposing; + InMemoryRegistryKeyData.RegistryChanged += OnRegistryChanged!; + inMemoryRegistryKey.Disposing += OnKeyDisposing!; // Handle potential race when registering the disposed event if (inMemoryRegistryKey.IsDisposed) @@ -73,8 +73,8 @@ private static async Task WaitForRegistryKeyChangeAsync( } finally { - InMemoryRegistryKeyData.RegistryChanged -= OnRegistryChanged; - inMemoryRegistryKey.Disposing -= OnKeyDisposing; + InMemoryRegistryKeyData.RegistryChanged -= OnRegistryChanged!; + inMemoryRegistryKey.Disposing -= OnKeyDisposing!; } return; diff --git a/src/CommonUtilities.Registry/src/RegistryKeyBase.cs b/src/CommonUtilities.Registry/src/RegistryKeyBase.cs index a0eefaf..c11dd1d 100644 --- a/src/CommonUtilities.Registry/src/RegistryKeyBase.cs +++ b/src/CommonUtilities.Registry/src/RegistryKeyBase.cs @@ -1,6 +1,8 @@ using System; using System.Globalization; +#if NETSTANDARD2_0 using System.Linq; +#endif namespace AnakinRaW.CommonUtilities.Registry; @@ -67,7 +69,7 @@ public abstract class RegistryKeyBase : IRegistryKey var nonNullableType = Nullable.GetUnderlyingType(type) ?? type; if (nonNullableType.IsEnum) - return (T)Enum.Parse(nonNullableType, result.ToString(), true); + return (T)Enum.Parse(nonNullableType, result.ToString()!, true); return (T)Convert.ChangeType(result, nonNullableType, CultureInfo.InvariantCulture); } diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index 6b2d33d..8234f1f 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -5,7 +5,7 @@ - netstandard2.0 + netstandard2.0;netstandard2.1;net10.0 enable true AnakinRaW.CommonUtilities.SimplePipeline @@ -20,13 +20,16 @@ true + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs index 40c3f69..78dcb1e 100644 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs @@ -103,12 +103,12 @@ protected override async Task RunCoreAsync(CancellationToken token) { try { - _stepRunner.Error += OnError; + _stepRunner.Error += OnError!; await _stepRunner.RunAsync(token).ConfigureAwait(false); } finally { - _stepRunner.Error -= OnError; + _stepRunner.Error -= OnError!; } if (!PipelineFailed) diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs index 0e27fa1..8087d43 100644 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs @@ -68,12 +68,12 @@ protected override async Task RunCoreAsync(CancellationToken token) { try { - _buildStepRunner.Error += OnError; + _buildStepRunner.Error += OnError!; await _buildStepRunner.RunAsync(token).ConfigureAwait(false); } finally { - _buildStepRunner.Error -= OnError; + _buildStepRunner.Error -= OnError!; } if (!PipelineFailed) diff --git a/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs b/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs index 22fbd48..95246ae 100644 --- a/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs +++ b/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs @@ -110,7 +110,7 @@ protected AggregatedProgressReporter( { if (!_progressSteps.Add(step)) continue; - step.Progress += OnStepProgress; + step.Progress += OnStepProgress!; TotalSize += step.Size; } } @@ -134,7 +134,7 @@ private void OnStepProgress(object sender, ProgressEventArgs e) protected override void DisposeResources() { foreach (var step in _progressSteps) - step.Progress -= OnStepProgress; + step.Progress -= OnStepProgress!; _progressSteps.Clear(); } } \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs b/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs index cf077a4..104211c 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs +++ b/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs @@ -261,7 +261,7 @@ public void ICollection_Generic_Add_AfterRemovingAllItems(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void ICollection_Generic_Add_ToReadOnlyFrugalList(int count) + public void ICollection_Generic_Add_ToReadOnlyCollection(int count) { if (IsReadOnly || AddRemoveClear_ThrowsNotSupported) { diff --git a/src/CommonUtilities.TestingUtilities/Collections/IEnumerableTestSuite.cs b/src/CommonUtilities.TestingUtilities/Collections/IEnumerableTestSuite.cs index 868d1c2..58993a7 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/IEnumerableTestSuite.cs +++ b/src/CommonUtilities.TestingUtilities/Collections/IEnumerableTestSuite.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Diagnostics.CodeAnalysis; namespace AnakinRaW.CommonUtilities.Testing.Collections; @@ -11,8 +12,17 @@ namespace AnakinRaW.CommonUtilities.Testing.Collections; [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] [SuppressMessage("ReSharper", "AccessToDisposedClosure")] [SuppressMessage("ReSharper", "InconsistentNaming")] -public abstract class IEnumerableTestSuite : INonModifyingEnumerableTestSuite +public abstract class IEnumerableTestSuite : CollectionsTestSuite { + /// + /// An enum to allow specification of the order of the Enumerable. Used in validation for enumerables. + /// + protected enum EnumerableOrder + { + Unspecified, + Sequential + } + /// /// Modifies the given IEnumerable such that any enumerators for that IEnumerable will be /// invalidated. @@ -21,6 +31,70 @@ public abstract class IEnumerableTestSuite : INonModifyingEnumerableTestSuite /// true if the enumerable was successfully modified. Else false. public delegate bool ModifyEnumerable(IEnumerable enumerable); + /// + /// The Reset method is provided for COM interoperability. It does not necessarily need to be + /// implemented; instead, the implementer can simply throw a NotSupportedException. + /// + /// If Reset is not implemented, this property must return False. The default value is true. + /// + protected virtual bool ResetImplemented => true; + + /// Whether the enumerator returned from GetEnumerator is a singleton instance when the collection is empty. + protected virtual bool Enumerator_Empty_UsesSingletonInstance => false; + + /// + /// When calling Current of the enumerator before the first MoveNext, after the end of the collection, + /// or after modification of the enumeration, the resulting behavior is undefined. Tests are included + /// to cover two behavioral scenarios: + /// - Throwing an InvalidOperationException + /// - Returning an undefined value. + /// + /// If this property is set to true, the tests ensure that the exception is thrown. The default value is + /// false. + /// + protected virtual bool Enumerator_Current_UndefinedOperation_Throws => false; + + /// + /// When calling Current of the enumerator before the first MoveNext, after the end of the collection, + /// or after modification of the enumeration, the resulting behavior is undefined. Tests are included + /// to cover two behavioral scenarios: + /// - Throwing an InvalidOperationException + /// - Returning an undefined value. + /// + /// If this property is set to true, the tests ensure that the exception is thrown. The default value is + /// false. + /// + protected virtual bool NonGenericEnumerator_Current_UndefinedOperation_Throws => false; + + /// + /// When calling Current of the empty enumerator before the first MoveNext, after the end of the collection, + /// or after modification of the enumeration, the resulting behavior is undefined. Tests are included + /// to cover two behavioral scenarios: + /// - Throwing an InvalidOperationException + /// - Returning an undefined value. + /// + /// If this property is set to true, the tests ensure that the exception is thrown. The default value is + /// . + /// + protected virtual bool Enumerator_Empty_Current_UndefinedOperation_Throws => Enumerator_Current_UndefinedOperation_Throws; + + /// + /// When calling Current of the empty enumerator before the first MoveNext, after the end of the collection, + /// or after modification of the enumeration, the resulting behavior is undefined. Tests are included + /// to cover two behavioral scenarios: + /// - Throwing an InvalidOperationException + /// - Returning an undefined value. + /// + /// If this property is set to true, the tests ensure that the exception is thrown. The default value is + /// false. + /// + protected virtual bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => Enumerator_Current_UndefinedOperation_Throws; + + /// + /// Specifies whether this IEnumerable follows some sort of ordering pattern. + /// + protected virtual EnumerableOrder Order => EnumerableOrder.Sequential; + /// /// When calling MoveNext or Reset after modification of the enumeration, the resulting behavior is /// undefined. Tests are included to cover two behavioral scenarios: @@ -47,14 +121,420 @@ public abstract class IEnumerableTestSuite : INonModifyingEnumerableTestSuite protected virtual ModifyOperation ModifyEnumeratorAllowed => ModifyOperation.None; + /// + /// Creates an instance of an IEnumerable{T} that can be used for testing. + /// + /// The number of unique items that the returned IEnumerable{T} contains. + /// An instance of an IEnumerable{T} that can be used for testing. + protected abstract IEnumerable GenericIEnumerableFactory(int count); + /// /// To be implemented in the concrete collections test classes. Returns a set of ModifyEnumerable delegates /// that modify the enumerable passed to them. /// protected abstract IEnumerable GetModifyEnumerables(ModifyOperation operations); - + + /// + /// Creates an instance of an IEnumerable that can be used for testing. + /// + /// The number of unique items that the returned IEnumerable contains. + /// An instance of an IEnumerable that can be used for testing. + protected virtual IEnumerable NonGenericIEnumerableFactory(int count) + { + return GenericIEnumerableFactory(count); + } + + private void RepeatTest(Action, T[]> testCode, int iters = 3) + { + RepeatTest((e, i, _) => testCode(e, i), iters); + } + + private void RepeatTest(Action, T[], int> testCode, int iters = 3) + { + var enumerable = GenericIEnumerableFactory(32); + var items = enumerable.ToArray(); + var enumerator = enumerable.GetEnumerator(); + for (var i = 0; i < iters; i++) + { + testCode(enumerator, items, i); + if (!ResetImplemented) + enumerator = enumerable.GetEnumerator(); + else + enumerator.Reset(); + } + enumerator.Dispose(); + } + + private void VerifyEnumerator(IEnumerator enumerator, T[] expectedItems) + { + VerifyEnumerator(enumerator, expectedItems, 0, expectedItems.Length, true, true); + } + + private void VerifyEnumerator(IEnumerator enumerator, T[] expectedItems, int startIndex, int count, bool validateStart, bool validateEnd) + { + var needToMatchAllExpectedItems = count - startIndex == expectedItems.Length; + if (validateStart) + { + for (var i = 0; i < 3; i++) + { + if (Enumerator_Current_UndefinedOperation_Throws) + { + Assert.Throws(() => enumerator.Current); + } + else + { + _ = enumerator.Current; + } + } + } + + int iterations; + if (Order == EnumerableOrder.Unspecified) + { + var itemsVisited = new BitArray(needToMatchAllExpectedItems ? count : expectedItems.Length, false); + for (iterations = 0; iterations < count && enumerator.MoveNext(); iterations++) + { + object? currentItem = enumerator.Current; + + var itemFound = false; + for (var i = 0; i < itemsVisited.Length; ++i) + { + if (!itemsVisited[i] && Equals(currentItem, expectedItems[i + (needToMatchAllExpectedItems ? startIndex : 0)])) + { + itemsVisited[i] = true; + itemFound = true; + break; + } + } + + Assert.True(itemFound, "itemFound"); + + for (var i = 0; i < 3; i++) + { + object? tempItem = enumerator.Current; + Assert.Equal(currentItem, tempItem); + } + } + + if (needToMatchAllExpectedItems) + { + for (var i = 0; i < itemsVisited.Length; i++) + { + Assert.True(itemsVisited[i]); + } + } + else + { + var visitedItemCount = 0; + for (var i = 0; i < itemsVisited.Length; i++) + { + if (itemsVisited[i]) + { + ++visitedItemCount; + } + } + + Assert.Equal(count, visitedItemCount); + } + } + else if (Order == EnumerableOrder.Sequential) + { + for (iterations = 0; iterations < count && enumerator.MoveNext(); iterations++) + { + object? currentItem = enumerator.Current; + Assert.Equal(expectedItems[iterations], currentItem); + for (var i = 0; i < 3; i++) + { + object? tempItem = enumerator.Current; + Assert.Equal(currentItem, tempItem); + } + } + } + else + { + throw new ArgumentException( + "EnumerableOrder is invalid."); + } + + Assert.Equal(count, iterations); + + if (validateEnd) + { + for (var i = 0; i < 3; i++) + { + Assert.False(enumerator.MoveNext(), "enumerator.MoveNext() returned true past the expected end."); + + if (Enumerator_Current_UndefinedOperation_Throws) + Assert.Throws(() => enumerator.Current); + else + _ = enumerator.Current; + } + } + } + + + #region GetEnumerator() + + [Fact] + public void IEnumerable_NonGeneric_GetEnumerator_EmptyCollection_UsesSingleton() + { + var enumerable = NonGenericIEnumerableFactory(0); + + var enumerator1 = enumerable.GetEnumerator(); + try + { + var enumerator2 = enumerable.GetEnumerator(); + try + { + Assert.Equal(Enumerator_Empty_UsesSingletonInstance, ReferenceEquals(enumerator1, enumerator2)); + } + finally + { + if (enumerator2 is IDisposable d2) d2.Dispose(); + } + } + finally + { + if (enumerator1 is IDisposable d1) d1.Dispose(); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_GetEnumerator_NoExceptionsWhileGetting(int count) + { + var enumerable = NonGenericIEnumerableFactory(count); + // ReSharper disable once GenericEnumeratorNotDisposed + Assert.NotNull(enumerable.GetEnumerator()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_GetEnumerator_ReturnsUniqueEnumerator(int count) + { + //Tests that the enumerators returned by GetEnumerator operate independently of one another + var enumerable = NonGenericIEnumerableFactory(count); + var iterations = 0; + foreach (var _ in enumerable) + foreach (var __ in enumerable) + foreach (var ___ in enumerable) + iterations++; + Assert.Equal(count * count * count, iterations); + } + + [Fact] + public void IEnumerable_Generic_GetEnumerator_EmptyCollection_UsesSingleton() + { + IEnumerable enumerable = GenericIEnumerableFactory(0); + + var enumerator1 = enumerable.GetEnumerator(); + try + { + var enumerator2 = enumerable.GetEnumerator(); + try + { + Assert.Equal(Enumerator_Empty_UsesSingletonInstance, ReferenceEquals(enumerator1, enumerator2)); + } + finally + { + if (enumerator2 is IDisposable d2) d2.Dispose(); + } + } + finally + { + if (enumerator1 is IDisposable d1) d1.Dispose(); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_GetEnumerator_NoExceptionsWhileGetting(int count) + { + var enumerable = GenericIEnumerableFactory(count); + enumerable.GetEnumerator().Dispose(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_GetEnumerator_ReturnsUniqueEnumerator(int count) + { + //Tests that the enumerators returned by GetEnumerator operate independently of one another + var enumerable = GenericIEnumerableFactory(count); + var iterations = 0; + foreach (var _ in enumerable) + foreach (var __ in enumerable) + foreach (var ___ in enumerable) + iterations++; + Assert.Equal(count * count * count, iterations); + } + + #endregion + #region Enumerator.MoveNext + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_MoveNext_FromStartToFinish(int count) + { + var iterations = 0; + var enumerator = NonGenericIEnumerableFactory(count).GetEnumerator(); + while (enumerator.MoveNext()) + iterations++; + Assert.Equal(count, iterations); + if (enumerator is IDisposable d) + d.Dispose(); + } + + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_MoveNext_AfterEndOfCollection(int count) + { + var enumerator = NonGenericIEnumerableFactory(count).GetEnumerator(); + for (var i = 0; i < count; i++) + enumerator.MoveNext(); + Assert.False(enumerator.MoveNext()); + Assert.False(enumerator.MoveNext()); + if (enumerator is IDisposable d) + d.Dispose(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_MoveNext_ModifiedBeforeEnumeration_ThrowsInvalidOperationException(int count) + { + Assert.All(GetModifyEnumerables(ModifyEnumeratorThrows), ModifyEnumerable => + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + if (ModifyEnumerable((IEnumerable)enumerable)) + { + if (count == 0 + ? Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException + : Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException) + Assert.Throws(() => enumerator.MoveNext()); + else + _ = enumerator.MoveNext(); + } + if (enumerator is IDisposable d) + d.Dispose(); + }); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_MoveNext_ModifiedDuringEnumeration_ThrowsInvalidOperationException(int count) + { + Assert.All(GetModifyEnumerables(ModifyEnumeratorThrows), ModifyEnumerable => + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + + for (var i = 0; i < count / 2; i++) + enumerator.MoveNext(); + + if (ModifyEnumerable((IEnumerable)enumerable)) + { + if (count == 0 + ? Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException + : Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException) + Assert.Throws(() => enumerator.MoveNext()); + else + enumerator.MoveNext(); + } + if (enumerator is IDisposable d) + d.Dispose(); + }); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_MoveNext_ModifiedAfterEnumeration_ThrowsInvalidOperationException(int count) + { + Assert.All(GetModifyEnumerables(ModifyEnumeratorThrows), ModifyEnumerable => + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + while (enumerator.MoveNext()) ; + if (ModifyEnumerable((IEnumerable)enumerable)) + { + if (count == 0 + ? Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException + : Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException) + Assert.Throws(() => enumerator.MoveNext()); + else + _ = enumerator.MoveNext(); + } + if (enumerator is IDisposable d) + d.Dispose(); + }); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_Enumerator_MoveNext_FromStartToFinish(int count) + { + var iterations = 0; + using var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); + while (enumerator.MoveNext()) + iterations++; + Assert.Equal(count, iterations); + } + + /// + /// For most collections, all calls to MoveNext after disposal of an enumerator will return false. + /// Some collections (SortedList), however, treat a call to dispose as if it were a call to Reset. Since the docs + /// specify neither of these as being strictly correct, we leave the method virtual. + /// + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public virtual void Enumerator_MoveNext_AfterDisposal(int count) + { + var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); + for (var i = 0; i < count; i++) + enumerator.MoveNext(); + enumerator.Dispose(); + Assert.False(enumerator.MoveNext()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_Enumerator_MoveNext_AfterEndOfCollection(int count) + { + using var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); + for (var i = 0; i < count; i++) + enumerator.MoveNext(); + Assert.False(enumerator.MoveNext()); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void IEnumerable_Generic_Enumerator_MoveNextHitsAllItems() + { + RepeatTest((enumerator, items) => + { + var iterations = 0; + while (enumerator.MoveNext()) + { + iterations++; + } + Assert.Equal(items.Length, iterations); + }); + } + + [Fact] + public void IEnumerable_Generic_Enumerator_MoveNextFalseAfterEndOfCollection() + { + RepeatTest((enumerator, _) => + { + while (enumerator.MoveNext()) + { + } + + Assert.False(enumerator.MoveNext()); + }); + } + [Theory] [MemberData(nameof(ValidCollectionSizes))] public void IEnumerable_Generic_Enumerator_MoveNext_ModifiedBeforeEnumeration_ThrowsInvalidOperationException(int count) @@ -185,6 +665,176 @@ public void IEnumerable_Generic_Enumerator_MoveNext_ModifiedAfterEnumeration_Suc #region Enumerator.Current + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_Current_FromStartToFinish(int count) + { + var enumerator = NonGenericIEnumerableFactory(count).GetEnumerator(); + while (enumerator.MoveNext()) + _ = enumerator.Current; + if (enumerator is IDisposable d) + d.Dispose(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_Current_ReturnsSameValueOnRepeatedCalls(int count) + { + var enumerator = NonGenericIEnumerableFactory(count).GetEnumerator(); + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + Assert.Equal(current, enumerator.Current); + Assert.Equal(current, enumerator.Current); + Assert.Equal(current, enumerator.Current); + } + if (enumerator is IDisposable d) + d.Dispose(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_Current_ReturnsSameObjectsOnDifferentEnumerators(int count) + { + // Ensures that the elements returned from enumeration are exactly the same collection of + // elements returned from a previous enumeration + var enumerable = NonGenericIEnumerableFactory(count); + var comparer = GetIEqualityComparer(); + var firstValues = new Dictionary(count, comparer); + var secondValues = new Dictionary(count, comparer); + foreach (T item in enumerable) + firstValues[item] = firstValues.ContainsKey(item) ? firstValues[item]++ : 1; + foreach (T item in enumerable) + secondValues[item] = secondValues.ContainsKey(item) ? secondValues[item]++ : 1; + Assert.Equal(firstValues.Count, secondValues.Count); + foreach (var key in firstValues.Keys) + Assert.Equal(firstValues[key], secondValues[key]); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public virtual void Enumerator_Current_BeforeFirstMoveNext_UndefinedBehavior(int count) + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + if (count == 0 ? NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw : NonGenericEnumerator_Current_UndefinedOperation_Throws) + Assert.Throws(() => enumerator.Current); + else + _ = enumerator.Current; + if (enumerator is IDisposable d) + d.Dispose(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public virtual void Enumerator_Current_AfterEndOfEnumerable_UndefinedBehavior(int count) + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + while (enumerator.MoveNext()) ; + if (count == 0 ? NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw : NonGenericEnumerator_Current_UndefinedOperation_Throws) + Assert.Throws(() => enumerator.Current); + else + _ = enumerator.Current; + if (enumerator is IDisposable d) + d.Dispose(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public virtual void Enumerator_Current_ModifiedDuringEnumeration_UndefinedBehavior(int count) + { + Assert.All(GetModifyEnumerables(ModifyEnumeratorThrows), ModifyEnumerable => + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + if (ModifyEnumerable((IEnumerable)enumerable)) + { + if (count == 0 ? NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw : NonGenericEnumerator_Current_UndefinedOperation_Throws) + Assert.Throws(() => enumerator.Current); + else + _ = enumerator.Current; + } + if (enumerator is IDisposable d) + d.Dispose(); + }); + } + + [Fact] + public void IEnumerable_Generic_Enumerator_Current() + { + // Verify that current returns proper result. + RepeatTest((enumerator, items, iteration) => + { + if (iteration == 1) + VerifyEnumerator(enumerator, items, 0, items.Length / 2, true, false); + else + VerifyEnumerator(enumerator, items); + }); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_Enumerator_Current_ReturnsSameValueOnRepeatedCalls(int count) + { + using var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + Assert.Equal(current, enumerator.Current); + Assert.Equal(current, enumerator.Current); + Assert.Equal(current, enumerator.Current); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_Enumerator_Current_ReturnsSameObjectsOnDifferentEnumerators(int count) + { + // Ensures that the elements returned from enumeration are exactly the same collection of + // elements returned from a previous enumeration + var enumerable = GenericIEnumerableFactory(count); + var comparer = GetIEqualityComparer(); +#pragma warning disable CS8714 + var firstValues = new Dictionary(count, comparer); + var secondValues = new Dictionary(count, comparer); +#pragma warning restore CS8714 + foreach (var item in enumerable) + firstValues[item] = firstValues.ContainsKey(item) ? firstValues[item]++ : 1; + foreach (var item in enumerable) + secondValues[item] = secondValues.ContainsKey(item) ? secondValues[item]++ : 1; + Assert.Equal(firstValues.Count, secondValues.Count); + foreach (var key in firstValues.Keys) + Assert.Equal(firstValues[key], secondValues[key]); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_Enumerator_Current_BeforeFirstMoveNext_UndefinedBehavior(int count) + { + var enumerable = GenericIEnumerableFactory(count); + using var enumerator = enumerable.GetEnumerator(); + if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throws : Enumerator_Current_UndefinedOperation_Throws) + Assert.Throws(() => enumerator.Current); + else + _ = enumerator.Current; + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_Enumerator_Current_AfterEndOfEnumerable_UndefinedBehavior(int count) + { + var enumerable = GenericIEnumerableFactory(count); + using var enumerator = enumerable.GetEnumerator(); + while (enumerator.MoveNext()) + { + } + + if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throws : Enumerator_Current_UndefinedOperation_Throws) + Assert.Throws(() => enumerator.Current); + else + _ = enumerator.Current; + } [Theory] [MemberData(nameof(ValidCollectionSizes))] @@ -223,6 +873,162 @@ public void IEnumerable_Generic_Enumerator_Current_ModifiedDuringEnumeration_Suc #region Enumerator.Reset + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_Reset_BeforeIteration_Support(int count) + { + var enumerator = NonGenericIEnumerableFactory(count).GetEnumerator(); + if (ResetImplemented) + enumerator.Reset(); + else + Assert.Throws(() => enumerator.Reset()); + if (enumerator is IDisposable d) + d.Dispose(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_Reset_ModifiedBeforeEnumeration_ThrowsInvalidOperationException(int count) + { + Assert.All(GetModifyEnumerables(ModifyEnumeratorThrows), ModifyEnumerable => + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + if (ModifyEnumerable((IEnumerable)enumerable)) + { + if (count == 0 + ? Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException + : Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException) + Assert.Throws(() => enumerator.Reset()); + else + enumerator.Reset(); + } + if (enumerator is IDisposable d) + d.Dispose(); + }); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_Reset_ModifiedDuringEnumeration_ThrowsInvalidOperationException(int count) + { + Assert.All(GetModifyEnumerables(ModifyEnumeratorThrows), ModifyEnumerable => + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + + for (var i = 0; i < count / 2; i++) + enumerator.MoveNext(); + + if (ModifyEnumerable((IEnumerable)enumerable)) + { + if (count == 0 + ? Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException + : Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException) + Assert.Throws(() => enumerator.Reset()); + else + enumerator.Reset(); + } + if (enumerator is IDisposable d) + d.Dispose(); + }); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_NonGeneric_Enumerator_Reset_ModifiedAfterEnumeration_ThrowsInvalidOperationException(int count) + { + Assert.All(GetModifyEnumerables(ModifyEnumeratorThrows), ModifyEnumerable => + { + var enumerable = NonGenericIEnumerableFactory(count); + var enumerator = enumerable.GetEnumerator(); + while (enumerator.MoveNext()) ; + if (ModifyEnumerable((IEnumerable)enumerable)) + { + if (count == 0 + ? Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException + : Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException) + Assert.Throws(() => enumerator.Reset()); + else + enumerator.Reset(); + } + if (enumerator is IDisposable d) + d.Dispose(); + }); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IEnumerable_Generic_Enumerator_Reset_BeforeIteration_Support(int count) + { + using var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); + if (ResetImplemented) + enumerator.Reset(); + else + Assert.Throws(enumerator.Reset); + } + + [Fact] + public void IEnumerable_Generic_Enumerator_Reset() + { + if (!ResetImplemented) + { + RepeatTest((enumerator, _) => + { + Assert.Throws(enumerator.Reset); + }); + RepeatTest((enumerator, items, iter) => + { + if (iter == 1) + { + VerifyEnumerator(enumerator, items, 0, items.Length / 2, true, false); + for (var i = 0; i < 3; i++) + { + Assert.Throws(enumerator.Reset); + } + + VerifyEnumerator(enumerator, items, items.Length / 2, items.Length - items.Length / 2, false, true); + } + else if (iter == 2) + { + VerifyEnumerator(enumerator, items); + for (var i = 0; i < 3; i++) + { + Assert.Throws(enumerator.Reset); + } + + VerifyEnumerator(enumerator, items, 0, 0, false, true); + } + else + { + VerifyEnumerator(enumerator, items); + } + }); + } + else + { + RepeatTest((enumerator, items, iter) => + { + if (iter == 1) + { + VerifyEnumerator(enumerator, items, 0, items.Length / 2, true, false); + enumerator.Reset(); + enumerator.Reset(); + } + else if (iter == 3) + { + VerifyEnumerator(enumerator, items); + enumerator.Reset(); + enumerator.Reset(); + } + else + { + VerifyEnumerator(enumerator, items); + } + }, 5); + } + } + [Theory] [MemberData(nameof(ValidCollectionSizes))] public void IEnumerable_Generic_Enumerator_Reset_ModifiedBeforeEnumeration_ThrowsInvalidOperationException(int count) @@ -339,7 +1145,7 @@ public void IEnumerable_Generic_Enumerator_Reset_ModifiedAfterEnumeration_Succee { } - if (modifyEnumerable(enumerable)) + if (modifyEnumerable(enumerable)) enumerator.Reset(); } } diff --git a/src/CommonUtilities.TestingUtilities/Collections/INonModifyingEnumerableTestSuite.cs b/src/CommonUtilities.TestingUtilities/Collections/INonModifyingEnumerableTestSuite.cs deleted file mode 100644 index a273bd5..0000000 --- a/src/CommonUtilities.TestingUtilities/Collections/INonModifyingEnumerableTestSuite.cs +++ /dev/null @@ -1,489 +0,0 @@ -using System.Collections; -using System.Diagnostics.CodeAnalysis; - -namespace AnakinRaW.CommonUtilities.Testing.Collections; - - -// This test suite is taken from the .NET runtime repository (https://github.com/dotnet/runtime) and adapted to the VSTesting Framework. -// The .NET Foundation licenses this under the MIT license. - -/// -/// Contains tests that ensure the correctness of any class that implements the generic -/// IEnumerable interface. -/// -[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] -[SuppressMessage("ReSharper", "AccessToDisposedClosure")] -[SuppressMessage("ReSharper", "InconsistentNaming")] -public abstract class INonModifyingEnumerableTestSuite : CollectionsTestSuite -{ - /// - /// An enum to allow specification of the order of the Enumerable. Used in validation for enumerables. - /// - protected enum EnumerableOrder - { - Unspecified, - Sequential - } - - /// - /// The Reset method is provided for COM interoperability. It does not necessarily need to be - /// implemented; instead, the implementer can simply throw a NotSupportedException. - /// - /// If Reset is not implemented, this property must return False. The default value is true. - /// - protected virtual bool ResetImplemented => true; - - /// Whether the enumerator returned from GetEnumerator is a singleton instance when the collection is empty. - protected virtual bool Enumerator_Empty_UsesSingletonInstance => false; - - /// - /// When calling Current of the enumerator before the first MoveNext, after the end of the collection, - /// or after modification of the enumeration, the resulting behavior is undefined. Tests are included - /// to cover two behavioral scenarios: - /// - Throwing an InvalidOperationException - /// - Returning an undefined value. - /// - /// If this property is set to true, the tests ensure that the exception is thrown. The default value is - /// false. - /// - protected virtual bool Enumerator_Current_UndefinedOperation_Throws => false; - - /// - /// When calling Current of the empty enumerator before the first MoveNext, after the end of the collection, - /// or after modification of the enumeration, the resulting behavior is undefined. Tests are included - /// to cover two behavioral scenarios: - /// - Throwing an InvalidOperationException - /// - Returning an undefined value. - /// - /// If this property is set to true, the tests ensure that the exception is thrown. The default value is - /// . - /// - protected virtual bool Enumerator_Empty_Current_UndefinedOperation_Throws => Enumerator_Current_UndefinedOperation_Throws; - - /// - /// Specifies whether this IEnumerable follows some sort of ordering pattern. - /// - protected virtual EnumerableOrder Order => EnumerableOrder.Sequential; - - /// - /// Creates an instance of an IEnumerable{T} that can be used for testing. - /// - /// The number of unique items that the returned IEnumerable{T} contains. - /// An instance of an IEnumerable{T} that can be used for testing. - protected abstract IEnumerable GenericIEnumerableFactory(int count); - - - private void RepeatTest(Action, T[]> testCode, int iters = 3) - { - RepeatTest((e, i, _) => testCode(e, i), iters); - } - - private void RepeatTest(Action, T[], int> testCode, int iters = 3) - { - var enumerable = GenericIEnumerableFactory(32); - var items = enumerable.ToArray(); - var enumerator = enumerable.GetEnumerator(); - for (var i = 0; i < iters; i++) - { - testCode(enumerator, items, i); - if (!ResetImplemented) - { - enumerator = enumerable.GetEnumerator(); - } - else - { - enumerator.Reset(); - } - } - } - - private void VerifyEnumerator(IEnumerator enumerator, T[] expectedItems) - { - VerifyEnumerator(enumerator, expectedItems, 0, expectedItems.Length, true, true); - } - - private void VerifyEnumerator(IEnumerator enumerator, T[] expectedItems, int startIndex, int count, bool validateStart, bool validateEnd) - { - var needToMatchAllExpectedItems = count - startIndex == expectedItems.Length; - if (validateStart) - { - for (var i = 0; i < 3; i++) - { - if (Enumerator_Current_UndefinedOperation_Throws) - { - Assert.Throws(() => enumerator.Current); - } - else - { - _ = enumerator.Current; - } - } - } - - int iterations; - if (Order == EnumerableOrder.Unspecified) - { - var itemsVisited = new BitArray(needToMatchAllExpectedItems ? count : expectedItems.Length, false); - for (iterations = 0; iterations < count && enumerator.MoveNext(); iterations++) - { - object? currentItem = enumerator.Current; - - var itemFound = false; - for (var i = 0; i < itemsVisited.Length; ++i) - { - if (!itemsVisited[i] && Equals(currentItem, expectedItems[i + (needToMatchAllExpectedItems ? startIndex : 0)])) - { - itemsVisited[i] = true; - itemFound = true; - break; - } - } - - Assert.True(itemFound, "itemFound"); - - for (var i = 0; i < 3; i++) - { - object? tempItem = enumerator.Current; - Assert.Equal(currentItem, tempItem); - } - } - - if (needToMatchAllExpectedItems) - { - for (var i = 0; i < itemsVisited.Length; i++) - { - Assert.True(itemsVisited[i]); - } - } - else - { - var visitedItemCount = 0; - for (var i = 0; i < itemsVisited.Length; i++) - { - if (itemsVisited[i]) - { - ++visitedItemCount; - } - } - - Assert.Equal(count, visitedItemCount); - } - } - else if (Order == EnumerableOrder.Sequential) - { - for (iterations = 0; iterations < count && enumerator.MoveNext(); iterations++) - { - object? currentItem = enumerator.Current; - Assert.Equal(expectedItems[iterations], currentItem); - for (var i = 0; i < 3; i++) - { - object? tempItem = enumerator.Current; - Assert.Equal(currentItem, tempItem); - } - } - } - else - { - throw new ArgumentException( - "EnumerableOrder is invalid."); - } - - Assert.Equal(count, iterations); - - if (validateEnd) - { - for (var i = 0; i < 3; i++) - { - Assert.False(enumerator.MoveNext(), "enumerator.MoveNext() returned true past the expected end."); - - if (Enumerator_Current_UndefinedOperation_Throws) - { - Assert.Throws(() => enumerator.Current); - } - else - { - _ = enumerator.Current; - } - } - } - } - - - #region GetEnumerator() - - [Fact] - public void IEnumerable_NonGeneric_GetEnumerator_EmptyCollection_UsesSingleton() - { - IEnumerable enumerable = GenericIEnumerableFactory(0); - - var enumerator1 = enumerable.GetEnumerator(); - try - { - var enumerator2 = enumerable.GetEnumerator(); - try - { - Assert.Equal(Enumerator_Empty_UsesSingletonInstance, ReferenceEquals(enumerator1, enumerator2)); - } - finally - { - if (enumerator2 is IDisposable d2) d2.Dispose(); - } - } - finally - { - if (enumerator1 is IDisposable d1) d1.Dispose(); - } - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_GetEnumerator_NoExceptionsWhileGetting(int count) - { - var enumerable = GenericIEnumerableFactory(count); - enumerable.GetEnumerator().Dispose(); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_GetEnumerator_ReturnsUniqueEnumerator(int count) - { - //Tests that the enumerators returned by GetEnumerator operate independently of one another - var enumerable = GenericIEnumerableFactory(count); - var iterations = 0; - foreach (var _ in enumerable) - foreach (var __ in enumerable) - foreach (var ___ in enumerable) - iterations++; - Assert.Equal(count * count * count, iterations); - } - - #endregion - - #region Enumerator.MoveNext - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_Enumerator_MoveNext_FromStartToFinish(int count) - { - var iterations = 0; - using var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); - while (enumerator.MoveNext()) - iterations++; - Assert.Equal(count, iterations); - } - - /// - /// For most collections, all calls to MoveNext after disposal of an enumerator will return false. - /// Some collections (SortedList), however, treat a call to dispose as if it were a call to Reset. Since the docs - /// specify neither of these as being strictly correct, we leave the method virtual. - /// - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public virtual void Enumerator_MoveNext_AfterDisposal(int count) - { - var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); - for (var i = 0; i < count; i++) - enumerator.MoveNext(); - enumerator.Dispose(); - Assert.False(enumerator.MoveNext()); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_Enumerator_MoveNext_AfterEndOfCollection(int count) - { - using var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); - for (var i = 0; i < count; i++) - enumerator.MoveNext(); - Assert.False(enumerator.MoveNext()); - Assert.False(enumerator.MoveNext()); - } - - [Fact] - public void IEnumerable_Generic_Enumerator_MoveNextHitsAllItems() - { - RepeatTest((enumerator, items) => - { - var iterations = 0; - while (enumerator.MoveNext()) - { - iterations++; - } - Assert.Equal(items.Length, iterations); - }); - } - - [Fact] - public void IEnumerable_Generic_Enumerator_MoveNextFalseAfterEndOfCollection() - { - RepeatTest((enumerator, _) => - { - while (enumerator.MoveNext()) - { - } - - Assert.False(enumerator.MoveNext()); - }); - } - - #endregion - - #region Enumerator.Current - - [Fact] - public void IEnumerable_Generic_Enumerator_Current() - { - // Verify that current returns proper result. - RepeatTest((enumerator, items, iteration) => - { - if (iteration == 1) - { - VerifyEnumerator(enumerator, items, 0, items.Length / 2, true, false); - } - else - { - VerifyEnumerator(enumerator, items); - } - }); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_Enumerator_Current_ReturnsSameValueOnRepeatedCalls(int count) - { - using var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); - while (enumerator.MoveNext()) - { - var current = enumerator.Current; - Assert.Equal(current, enumerator.Current); - Assert.Equal(current, enumerator.Current); - Assert.Equal(current, enumerator.Current); - } - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_Enumerator_Current_ReturnsSameObjectsOnDifferentEnumerators(int count) - { - // Ensures that the elements returned from enumeration are exactly the same collection of - // elements returned from a previous enumeration - var enumerable = GenericIEnumerableFactory(count); -#pragma warning disable CS8714 - var firstValues = new Dictionary(count); - var secondValues = new Dictionary(count); -#pragma warning restore CS8714 - foreach (var item in enumerable) - firstValues[item] = firstValues.ContainsKey(item) ? firstValues[item]++ : 1; - foreach (var item in enumerable) - secondValues[item] = secondValues.ContainsKey(item) ? secondValues[item]++ : 1; - Assert.Equal(firstValues.Count, secondValues.Count); - foreach (var key in firstValues.Keys) - Assert.Equal(firstValues[key], secondValues[key]); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_Enumerator_Current_BeforeFirstMoveNext_UndefinedBehavior(int count) - { - var enumerable = GenericIEnumerableFactory(count); - using var enumerator = enumerable.GetEnumerator(); - if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throws : Enumerator_Current_UndefinedOperation_Throws) - Assert.Throws(() => enumerator.Current); - else - _ = enumerator.Current; - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_Enumerator_Current_AfterEndOfEnumerable_UndefinedBehavior(int count) - { - var enumerable = GenericIEnumerableFactory(count); - using var enumerator = enumerable.GetEnumerator(); - while (enumerator.MoveNext()) - { - } - - if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throws : Enumerator_Current_UndefinedOperation_Throws) - Assert.Throws(() => enumerator.Current); - else - _ = enumerator.Current; - } - - #endregion - - #region Enumerator.Reset - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IEnumerable_Generic_Enumerator_Reset_BeforeIteration_Support(int count) - { - using var enumerator = GenericIEnumerableFactory(count).GetEnumerator(); - if (ResetImplemented) - enumerator.Reset(); - else - Assert.Throws(enumerator.Reset); - } - - [Fact] - public void IEnumerable_Generic_Enumerator_Reset() - { - if (!ResetImplemented) - { - RepeatTest((enumerator, _) => - { - Assert.Throws(enumerator.Reset); - }); - RepeatTest((enumerator, items, iter) => - { - if (iter == 1) - { - VerifyEnumerator(enumerator, items, 0, items.Length / 2, true, false); - for (var i = 0; i < 3; i++) - { - Assert.Throws(enumerator.Reset); - } - - VerifyEnumerator(enumerator, items, items.Length / 2, items.Length - items.Length / 2, false, true); - } - else if (iter == 2) - { - VerifyEnumerator(enumerator, items); - for (var i = 0; i < 3; i++) - { - Assert.Throws(enumerator.Reset); - } - - VerifyEnumerator(enumerator, items, 0, 0, false, true); - } - else - { - VerifyEnumerator(enumerator, items); - } - }); - } - else - { - RepeatTest((enumerator, items, iter) => - { - if (iter == 1) - { - VerifyEnumerator(enumerator, items, 0, items.Length / 2, true, false); - enumerator.Reset(); - enumerator.Reset(); - } - else if (iter == 3) - { - VerifyEnumerator(enumerator, items); - enumerator.Reset(); - enumerator.Reset(); - } - else - { - VerifyEnumerator(enumerator, items); - } - }, 5); - } - } - - #endregion -} \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyCollectionTestSuite.cs b/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyCollectionTestSuite.cs index b48018d..fe721f7 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyCollectionTestSuite.cs +++ b/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyCollectionTestSuite.cs @@ -9,7 +9,7 @@ namespace AnakinRaW.CommonUtilities.Testing.Collections; /// interface /// [SuppressMessage("ReSharper", "InconsistentNaming")] -public abstract class IReadOnlyCollectionTestSuite : INonModifyingEnumerableTestSuite +public abstract class IReadOnlyCollectionTestSuite : IEnumerableTestSuite { /// /// Creates an instance of an that can be used for testing. @@ -33,6 +33,11 @@ protected virtual IReadOnlyCollection GenericIReadOnlyCollectionFactory(int c return GenericIReadOnlyCollectionFactory(collection); } + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) + { + yield break; + } + #region Count [Theory] diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj index 084c890..2b0d8f8 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj @@ -17,23 +17,27 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/CommonUtilities.TestingUtilities/EqualityComparerConstantHashCode.cs b/src/CommonUtilities.TestingUtilities/EqualityComparerConstantHashCode.cs new file mode 100644 index 0000000..bde2db6 --- /dev/null +++ b/src/CommonUtilities.TestingUtilities/EqualityComparerConstantHashCode.cs @@ -0,0 +1,8 @@ +namespace AnakinRaW.CommonUtilities.Testing; + +public sealed class EqualityComparerConstantHashCode(IEqualityComparer comparer) : IEqualityComparer +{ + public bool Equals(T x, T y) => comparer.Equals(x, y); + + public int GetHashCode(T obj) => 42; +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/DebugViews.cs b/src/CommonUtilities/src/Collections/DebugViews.cs new file mode 100644 index 0000000..3589da2 --- /dev/null +++ b/src/CommonUtilities/src/Collections/DebugViews.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace AnakinRaW.CommonUtilities.Collections; + +internal sealed class IValueListDictionaryDebugView(IReadOnlyValueListDictionary dictionary) + where TKey : notnull +{ + private readonly IReadOnlyValueListDictionary _dict = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public DebugViewValueListDictionaryItem[] Items => _dict.Select(keyValuePair => new DebugViewValueListDictionaryItem(keyValuePair)).ToArray(); +} + +[DebuggerDisplay("{ValueList}", Name = "[{Key}]")] +internal readonly struct DebugViewValueListDictionaryItem(KeyValuePair> keyValue) +{ + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + public TKey Key { get; } = keyValue.Key; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public ReadOnlyFrugalList ValueList { get; } = keyValue.Value; +} + + +// From https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ICollectionDebugView.cs +internal sealed class IReadOnlyCollectionDebugView(IReadOnlyCollection collection) +{ + private readonly IReadOnlyCollection _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items => _collection.ToArray(); +} + +internal sealed class ICollectionDebugView(ICollection collection) +{ + private readonly ICollection _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + var items = new T[_collection.Count]; + _collection.CopyTo(items, 0); + return items; + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/FrugalList.cs b/src/CommonUtilities/src/Collections/FrugalList.cs index b4e489c..be8d03f 100644 --- a/src/CommonUtilities/src/Collections/FrugalList.cs +++ b/src/CommonUtilities/src/Collections/FrugalList.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace AnakinRaW.CommonUtilities.Collections; @@ -54,6 +55,8 @@ namespace AnakinRaW.CommonUtilities.Collections; /// /// /// The type of elements in the list. +[DebuggerTypeProxy(typeof(ICollectionDebugView<>))] +[DebuggerDisplay("Count = {Count}")] public struct FrugalList : IList { private static readonly EqualityComparer ItemComparer = EqualityComparer.Default; diff --git a/src/CommonUtilities/src/Collections/IReadOnlyValueListDictionary.cs b/src/CommonUtilities/src/Collections/IReadOnlyValueListDictionary.cs new file mode 100644 index 0000000..132bcea --- /dev/null +++ b/src/CommonUtilities/src/Collections/IReadOnlyValueListDictionary.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents a read-only generic collection that maps keys to list of values. +/// +/// +/// +/// Unlike a standard , this dictionary +/// allows multiple values to be associated with a single key. +/// +/// +/// When enumerating, each key appears exactly once with all its associated values +/// as a . +/// +/// +/// The type of keys in the dictionary. +/// The type of values in the dictionary. +public interface IReadOnlyValueListDictionary : IEnumerable>> where TKey : notnull +{ + /// + /// Gets the list of values associated with the specified key. + /// + /// The key of the values to get. + /// A containing all values for the specified key. + /// is . + /// The key does not exist in the dictionary. + ReadOnlyFrugalList this[TKey key] { get; } + + /// + /// Gets a collection containing all values in the dictionary. + /// + /// + /// + /// Returns a flattened collection of all values across all keys. + /// If a key has multiple values, each value appears separately in the collection. + /// + /// + /// Values appear in insertion order: first all values for the first key (in the order they were added), + /// then all values for the second key, and so on. + /// + /// + /// The collection count equals , not . + /// Modifications to the returned collection are not reflected in the dictionary. + /// + /// + /// To get values for a specific key without flattening, use . + /// + /// + ICollection Values { get; } + + /// + /// Gets an containing the keys in the dictionary. + /// + /// + /// Modifications to the collection are not reflected in the dictionary. + ///
+ /// The keys in the returned are ordered by their first insertion into the dictionary. + ///
+ ICollection Keys { get; } + + /// + /// Gets the total number of values across all keys in the dictionary. + /// + /// + /// This is the sum of all values for all keys, not the number of distinct keys. + /// Use to get the number of distinct keys. + /// + int Count { get; } + + /// + /// Gets the number of distinct keys in the dictionary. + /// + int KeyCount { get; } + + /// + /// Determines whether the dictionary contains the specified key. + /// + /// The key to locate in the dictionary. + /// if the dictionary contains the key; otherwise, . + /// is . + bool ContainsKey(TKey key); + + /// + /// Get a list of values stored with the specified key. + /// + /// The key to get the list of values for. + /// The list of values of the specified . + /// is . + /// The key does not exist in the dictionary. + ReadOnlyFrugalList GetValues(TKey key); + + /// + /// Gets the last element with the specified key. + /// + /// The key of the element to get. + /// The last element with the specified key. + /// is . + /// The key does not exist in the dictionary. + TValue GetLastValue(TKey key); + + /// + /// Gets the first element with the specified key. + /// + /// The key of the element to get. + /// The first element with the specified key. + /// is . + /// The key does not exist in the dictionary. + TValue GetFirstValue(TKey key); + + /// + /// Gets the first value associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, the first value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. + /// if the dictionary contains a value with the specified key; otherwise, . + /// is . + bool TryGetFirstValue(TKey key, [MaybeNullWhen(false)] out TValue value); + + /// + /// Gets the last value associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, the last value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. + /// if the dictionary contains a value with the specified key; otherwise, . + /// is . + bool TryGetLastValue(TKey key, [MaybeNullWhen(false)] out TValue value); + + /// + /// Gets the list of values associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, a list of values associated with the specified key, if the key is found; + /// otherwise, an empty list. This parameter is passed uninitialized. + /// if the dictionary contains at least one value with the specified key; otherwise, . + /// is . + bool TryGetValues(TKey key, out ReadOnlyFrugalList values); +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/IValueListDictionary.cs b/src/CommonUtilities/src/Collections/IValueListDictionary.cs new file mode 100644 index 0000000..38da396 --- /dev/null +++ b/src/CommonUtilities/src/Collections/IValueListDictionary.cs @@ -0,0 +1,59 @@ +using System; + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents a generic collection that maps keys to list of values, while maintaining the order of key insertion. +/// +/// The type of the keys in the dictionary. +/// The type of the value-list in the dictionary. +public interface IValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull +{ + /// + /// Adds a value to the dictionary under the specified key. + /// + /// + /// Multiple values can be added under the same key. Values are stored in insertion order. + /// + /// The key under which to add the value. + /// The value to add. + /// + /// if the key already existed and the value was added + /// to an existing key; if a new key was created. + /// + /// is . + bool Add(TKey key, TValue value); + + /// + /// Removes all values associated with the specified key from the dictionary. + /// + /// The key to remove. + /// if the key was found and removed; otherwise, . + /// is . + bool Remove(TKey key); + + /// + /// Removes a specific value associated with the specified key. + /// + /// The key whose value to remove. + /// The value to remove. + /// + /// if the value was found and removed; + /// if the key or value was not found. + /// + /// is . + /// + /// If this was the last value for the key, the key is also removed from the dictionary. + /// If multiple identical values exist for the key, only the first occurrence is removed. + /// + bool Remove(TKey key, TValue value); + + /// + /// Removes all keys and values from the . + /// + /// + /// and + /// are set to zero. + /// + void Clear(); +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs b/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs index 7b5119a..71ba813 100644 --- a/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs +++ b/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; namespace AnakinRaW.CommonUtilities.Collections; @@ -8,6 +9,8 @@ namespace AnakinRaW.CommonUtilities.Collections; /// A read-only variant of the . ///
/// The type of elements in the list. +[DebuggerTypeProxy(typeof(IReadOnlyCollectionDebugView<>))] +[DebuggerDisplay("Count = {Count}")] public readonly struct ReadOnlyFrugalList : IReadOnlyList { /// diff --git a/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionary.cs b/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionary.cs new file mode 100644 index 0000000..b79df16 --- /dev/null +++ b/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionary.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents a read-only, generic dictionary that maps keys to one or more values, while maintaining the order of key insertion. +/// +/// The type of keys in the dictionary. +/// The type of values in the dictionary. +[DebuggerTypeProxy(typeof(IValueListDictionaryDebugView<,>))] +[DebuggerDisplay("Count = {Count}")] +public class ReadOnlyValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull +{ + private readonly IValueListDictionary _dictionary; + + /// Gets an empty . + /// An empty . + /// The returned instance is immutable and will always be empty. + public static ReadOnlyValueListDictionary Empty { get; } = new(new ValueListDictionary()); + + /// + public ReadOnlyFrugalList this[TKey key] => _dictionary[key]; + + /// + /// Gets a key collection that contains the keys of the dictionary. + /// + public KeyCollection Keys => field ??= new KeyCollection(_dictionary.Keys); + + /// + /// Gets a collection that contains the values in the dictionary. + /// + public ValueCollection Values => field ??= new ValueCollection(_dictionary.Values); + + ICollection IReadOnlyValueListDictionary.Values => Values; + + ICollection IReadOnlyValueListDictionary.Keys => Keys; + + /// + public int Count => _dictionary.Count; + + /// + public int KeyCount => _dictionary.KeyCount; + + /// + /// Initializes a new instance of the class that is a wrapper around the specified value list dictionary. + /// + /// The dictionary to wrap. + /// is . + public ReadOnlyValueListDictionary(IValueListDictionary dictionary) + { + _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + } + + /// + public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); + + /// + public ReadOnlyFrugalList GetValues(TKey key) => _dictionary.GetValues(key); + + /// + public TValue GetLastValue(TKey key) => _dictionary.GetLastValue(key); + + /// + public TValue GetFirstValue(TKey key) => _dictionary.GetFirstValue(key); + + /// + public bool TryGetFirstValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetFirstValue(key, out value); + + /// + public bool TryGetLastValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetLastValue(key, out value); + + /// + public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) => _dictionary.TryGetValues(key, out values); + + /// + public IEnumerator>> GetEnumerator() => _dictionary.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Represents a read-only collection of the keys of a object. + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + public sealed class KeyCollection : ICollection, IReadOnlyCollection + { + private readonly ICollection _collection; + + /// + public int Count => _collection.Count; + + bool ICollection.IsReadOnly => true; + + internal KeyCollection(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + /// + public bool Contains(TKey item) => _collection.Contains(item); + + /// + public void CopyTo(TKey[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); + + /// + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + void ICollection.Add(TKey item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Remove(TKey item) => throw new NotSupportedException(); + } + + /// + /// Represents a read-only collection of the values of a object. + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + public sealed class ValueCollection : ICollection, IReadOnlyCollection + { + private readonly ICollection _collection; + + /// + public int Count => _collection.Count; + + bool ICollection.IsReadOnly => true; + + internal ValueCollection(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + bool ICollection.Contains(TValue item) => _collection.Contains(item); + + /// + public void CopyTo(TValue[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); + + /// + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + void ICollection.Add(TValue item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Remove(TValue item) => throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/ValueListDictionary.cs b/src/CommonUtilities/src/Collections/ValueListDictionary.cs new file mode 100644 index 0000000..7bee636 --- /dev/null +++ b/src/CommonUtilities/src/Collections/ValueListDictionary.cs @@ -0,0 +1,630 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +#if NET6_0_OR_GREATER +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#endif + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents a generic dictionary that maps keys to one or more values, while maintaining the order of key insertion. +/// +/// The type of the keys in the dictionary. Keys must be non-nullable. +/// The type of the values in the dictionary. +/// +/// +/// Unlike a standard , this dictionary allows multiple values +/// to be associated with a single key. Values are stored in the order they were added. +/// +/// +/// A can support multiple readers concurrently, +/// as long as the collection is not modified. Even so, enumerating through a collection is +/// intrinsically not a thread-safe procedure. In the rare case where an enumeration contends +/// with write accesses, the collection must be locked during the entire enumeration. +/// +/// +[DebuggerTypeProxy(typeof(IValueListDictionaryDebugView<,>))] +[DebuggerDisplay("Count = {Count}")] +public class ValueListDictionary : IValueListDictionary where TKey : notnull +{ + private readonly List _keyOrder = []; + private readonly Dictionary> _values; + + /// + public int Count { get; private set; } + + /// + public int KeyCount => _keyOrder.Count; + + /// + /// Gets a collection containing the keys in the . + /// + /// + /// A containing the keys in the . + /// + /// + /// + /// The keys in the are returned in the order they were first inserted. + /// + /// + /// The returned is not a static copy; instead, the + /// refers back to the keys in the original . + /// Therefore, changes to the continue to be + /// reflected in the . + /// + /// + public KeyCollection Keys => field ??= new KeyCollection(this); + + /// + ICollection IReadOnlyValueListDictionary.Keys => field ??= new KeyCollection(this); + + /// + public ReadOnlyFrugalList this[TKey key] => GetValues(key); + + /// + /// Gets a collection containing all values in the . + /// + /// + /// A containing all values in the . + /// + /// + /// + /// Returns a flattened collection of all values across all keys. + /// If a key has multiple values, each value appears separately in the collection. + /// + /// + /// Values appear in insertion order: first all values for the first key (in the order they were added), + /// then all values for the second key, and so on. + /// + /// + /// The collection count equals , not . + /// + /// + /// The returned is not a static copy; instead, it + /// refers back to the values in the original . + /// Therefore, changes to the continue to be + /// reflected in the . + /// + /// + /// To get values for a specific key without flattening, use . + /// + /// + public ValueCollection Values => field ??= new ValueCollection(this); + + /// + ICollection IReadOnlyValueListDictionary.Values => Values; + + /// + /// Initializes a new instance of the class + /// that is empty and uses the default equality comparer for the key type. + /// + public ValueListDictionary() : this(null) + { + } + + /// + /// Initializes a new instance of the class + /// that is empty and uses the specified . + /// + /// + /// The implementation to use when comparing keys, + /// or to use the default for the type of the key. + /// + public ValueListDictionary(IEqualityComparer? comparer) + { + _values = new Dictionary>(comparer ?? EqualityComparer.Default); + } + + /// + public bool Add(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + Count++; + +#if NET + ref var list = ref CollectionsMarshal.GetValueRefOrAddDefault(_values, key, out var exists); + list.Add(value); + + if (!exists) + _keyOrder.Add(key); + + return exists; +#else + if (_values.TryGetValue(key, out var list)) + { + list.Add(value); + _values[key] = list; + return true; + } + + _keyOrder.Add(key); + _values[key] = new FrugalList(value); + return false; +#endif + } + + /// + public bool Remove(TKey key) + { + if (_values.TryGetValue(key, out var list)) + { + Count -= list.Count; + _values.Remove(key); + _keyOrder.Remove(key); + return true; + } + + return false; + } + + /// + public bool Remove(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + +# if NET6_0_OR_GREATER + ref var list = ref CollectionsMarshal.GetValueRefOrNullRef(_values, key); + if (Unsafe.IsNullRef(ref list)) + return false; + + if (!list.Remove(value)) + return false; + + Count--; + + // If this was the last value, remove the key entirely + if (list.Count == 0) + { + _values.Remove(key); + _keyOrder.Remove(key); + } + + return true; +# else + if (!_values.TryGetValue(key, out var list)) + return false; + + if (!list.Remove(value)) + return false; + + Count--; + + // If this was the last value, remove the key entirely + if (list.Count == 0) + { + _values.Remove(key); + _keyOrder.Remove(key); + } + else + { + _values[key] = list; + } + + return true; +# endif + } + + /// + public void Clear() + { + _keyOrder.Clear(); + _values.Clear(); + Count = 0; + } + + /// + public bool ContainsKey(TKey key) => _values.ContainsKey(key); + + /// + public ReadOnlyFrugalList GetValues(TKey key) + { + if (_values.TryGetValue(key, out var list)) + return list.AsReadOnly(); + + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + /// + public TValue GetFirstValue(TKey key) + { + if (_values.TryGetValue(key, out var list)) + return list[0]; + + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + /// + public TValue GetLastValue(TKey key) + { + if (_values.TryGetValue(key, out var list)) +#if NETSTANDARD2_1_OR_GREATER || NET + return list[^1]; +#else + return list[list.Count - 1]; +#endif + + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + /// + public bool TryGetFirstValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (_values.TryGetValue(key, out var list)) + { + value = list[0]; + return true; + } + + value = default!; + return false; + } + + /// + public bool TryGetLastValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (_values.TryGetValue(key, out var list)) + { +#if NETSTANDARD2_1_OR_GREATER || NET + value = list[^1]; +#else + value = list[list.Count - 1]; +#endif + return true; + } + + value = default!; + return false; + } + + /// + public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) + { + if (_values.TryGetValue(key, out var list)) + { + values = list.AsReadOnly(); + return true; + } + + values = ReadOnlyFrugalList.Empty; + return false; + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// An for the . + /// + /// + /// The enumerator returns each key exactly once, paired with a + /// containing all values associated with that key. + /// + /// + /// Enumerators can be used to read the data in the collection, but they cannot be used to modify + /// the underlying collection. + /// + /// + public Enumerator GetEnumerator() => new(this); + + IEnumerator>> IEnumerable>>.GetEnumerator() + => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Enumerates the elements of a . + /// + /// + /// Each element is a where the key is unique + /// and the value is a containing all values for that key. + /// + public struct Enumerator : IEnumerator>> + { + private readonly ValueListDictionary _dictionary; + private int _index; + private KeyValuePair> _current; + + internal Enumerator(ValueListDictionary dictionary) + { + _dictionary = dictionary; + _index = 0; + _current = default; + } + + /// + public KeyValuePair> Current => _current; + + /// + object IEnumerator.Current + { + get + { + if (_index == 0 || _index == _dictionary._keyOrder.Count + 1) + throw new InvalidOperationException("Enumeration has either not started or has already finished."); + return Current; + } + } + + /// + public bool MoveNext() + { + if (_index < _dictionary._keyOrder.Count) + { + var key = _dictionary._keyOrder[_index]; + _current = new KeyValuePair>( + key, + _dictionary._values[key].AsReadOnly()); + _index++; + return true; + } + + _index = _dictionary._keyOrder.Count + 1; + _current = default; + return false; + } + + /// + public void Reset() + { + _index = 0; + _current = default; + } + + /// + public void Dispose() { } + } + + /// + /// Represents the collection of keys in a . + /// + /// + /// + /// The keys in the are returned in the order they were first inserted + /// into the . + /// + /// + /// The is not a static copy; instead, the + /// refers back to the keys in the original . + /// Therefore, changes to the continue to be + /// reflected in the . + /// + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + public sealed class KeyCollection : ICollection, IReadOnlyCollection + { + private readonly ValueListDictionary _dictionary; + + internal KeyCollection(ValueListDictionary dictionary) + { + _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + } + + /// + /// Gets the number of elements contained in the . + /// + /// The number of elements contained in the . + public int Count => _dictionary._keyOrder.Count; + + /// + /// Gets a value indicating whether the is read-only. + /// + /// Always returns . + public bool IsReadOnly => true; + + /// + /// Determines whether the contains a specific key. + /// + /// The key to locate in the . + /// + /// if is found in the ; + /// otherwise, . + /// + public bool Contains(TKey item) => _dictionary.ContainsKey(item); + + /// + public void CopyTo(TKey[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException(nameof(array)); + if (arrayIndex < 0) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + if (array.Length - arrayIndex < Count) + throw new ArgumentException("Destination array is not long enough."); + + _dictionary._keyOrder.CopyTo(array, arrayIndex); + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// A for the . + public List.Enumerator GetEnumerator() => _dictionary._keyOrder.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + void ICollection.Add(TKey item) => throw new NotSupportedException(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + void ICollection.Clear() => throw new NotSupportedException(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + bool ICollection.Remove(TKey item) => throw new NotSupportedException(); + } + + /// + /// Represents the collection of values in a . + /// + /// + /// + /// The values in the are returned grouped by key, in the order + /// the keys were first inserted into the . + /// Within each key group, values appear in the order they were added. + /// + /// + /// The is not a static copy; instead, the + /// refers back to the values in the original . + /// Therefore, changes to the continue to be + /// reflected in the . + /// + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + public sealed class ValueCollection : ICollection, IReadOnlyCollection + { + private readonly ValueListDictionary _dictionary; + + internal ValueCollection(ValueListDictionary dictionary) + { + _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + } + + /// + public int Count => _dictionary.Count; + + /// + /// Gets a value indicating whether the is read-only. + /// + /// Always returns . + public bool IsReadOnly => true; + + /// + public bool Contains(TValue item) + { + var comparer = EqualityComparer.Default; + foreach (var value in this) + { + if (comparer.Equals(value, item)) + return true; + } + return false; + } + + /// + public void CopyTo(TValue[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException(nameof(array)); + if (arrayIndex < 0) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + if (array.Length - arrayIndex < Count) + throw new ArgumentException("Destination array is not long enough."); + + foreach (var value in this) + array[arrayIndex++] = value; + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// An for the . + public Enumerator GetEnumerator() => new(_dictionary); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + void ICollection.Add(TValue item) => throw new NotSupportedException(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + void ICollection.Clear() => throw new NotSupportedException(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + bool ICollection.Remove(TValue item) => throw new NotSupportedException(); + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator : IEnumerator + { + private ValueListDictionary.Enumerator _dictEnumerator; + private FrugalList.FrugalEnumerator _valueEnumerator; + private bool _hasCurrentList; + + internal Enumerator(ValueListDictionary dictionary) + { + _dictEnumerator = dictionary.GetEnumerator(); + _valueEnumerator = default; + _hasCurrentList = false; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + /// The element in the at the current position of the enumerator. + public TValue Current => _valueEnumerator.Current; + + /// + object IEnumerator.Current => Current!; + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + // Try next value in current list + if (_hasCurrentList && _valueEnumerator.MoveNext()) + return true; + + // Move to next key-value group + while (_dictEnumerator.MoveNext()) + { + _valueEnumerator = _dictEnumerator.Current.Value.GetEnumerator(); + _hasCurrentList = true; + + if (_valueEnumerator.MoveNext()) + return true; + } + + return false; + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + public void Reset() + { + _dictEnumerator.Reset(); + _valueEnumerator = default; + _hasCurrentList = false; + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() => _dictEnumerator.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index f6180ae..36908c0 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -22,13 +22,16 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CommonUtilities/test/Collections/DebugViewTest.cs b/src/CommonUtilities/test/Collections/DebugViewTest.cs new file mode 100644 index 0000000..cdd98e3 --- /dev/null +++ b/src/CommonUtilities/test/Collections/DebugViewTest.cs @@ -0,0 +1,318 @@ +using AnakinRaW.CommonUtilities.Collections; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections; + +// From https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/System/Collections/DebugView.Tests.cs +// and https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/System/Diagnostics/DebuggerAttributes.cs + +public class DebugViewTests +{ + public static IEnumerable TestDebuggerAttributes_ValueListDictionaryInput() + { + yield return [new ValueListDictionary(), Array.Empty>()]; + yield return [new ReadOnlyValueListDictionary(new ValueListDictionary()), Array.Empty>()]; + + yield return + [ + new ValueListDictionary{{1, "One"}, {2, "Two"}, {1, " Three"}}, + new KeyValuePair[] + { + new ("[1]", "Count = 2"), + new ("[2]", "Count = 1"), + } + ]; + + yield return + [ + new ReadOnlyValueListDictionary(new ValueListDictionary{{1, "One"}, {2, "Two"}, {1, " Three"}}), + new KeyValuePair[] + { + new ("[1]", "Count = 2"), + new ("[2]", "Count = 1"), + } + ]; + } + + public static IEnumerable TestDebuggerAttributes_FrugalListsInput() + { + yield return [new FrugalList()]; + yield return [new FrugalList { 1, 2 }]; + + yield return [new ReadOnlyFrugalList()]; + yield return [new ReadOnlyFrugalList([1,2])]; + } + + public static IEnumerable TestDebuggerAttributes_Inputs() + { + return TestDebuggerAttributes_ValueListDictionaryInput() + .Select(t => new[] { t[0] }) + .Concat(TestDebuggerAttributes_FrugalListsInput()); + } + + [Theory] + [MemberData(nameof(TestDebuggerAttributes_ValueListDictionaryInput))] + public static void TestDebuggerAttributes_ValueListDictionary(IReadOnlyValueListDictionary obj, KeyValuePair[] expected) + { + DebuggerAttributes.ValidateDebuggerDisplayReferences(obj); + var info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(obj); + var itemProperty = info.Properties.Single(pr => pr.GetCustomAttribute()?.State == DebuggerBrowsableState.RootHidden); + var items = (DebugViewValueListDictionaryItem[])itemProperty.GetValue(info.Instance)!; + var formatted = items + .Select(DebuggerAttributes.ValidateDebugViewValueListDictionaryItem) + .Select(formattedResult => new KeyValuePair(formattedResult.Key, formattedResult.Value)) + .ToList(); + Assert.Equal(expected, formatted); + } + + [Theory] + [MemberData(nameof(TestDebuggerAttributes_FrugalListsInput))] + public static void TestDebuggerAttributes_FrugalList(IEnumerable obj) + { + DebuggerAttributes.ValidateDebuggerDisplayReferences(obj); + var info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(obj); + var itemProperty = info.Properties.Single(pr => pr.GetCustomAttribute()?.State == DebuggerBrowsableState.RootHidden); + var items = itemProperty.GetValue(info.Instance) as int[]; + Assert.Equal(obj, items); + } + + [Theory] + [MemberData(nameof(TestDebuggerAttributes_Inputs))] + public static void TestDebuggerAttributes_Null(object obj) + { + var tie = Assert.Throws(() => DebuggerAttributes.CreateDebuggerTypeProxyWithNullArgument(obj.GetType())); + Assert.IsType(tie.InnerException); + } + + internal static class DebuggerAttributes + { + internal class DebuggerAttributeInfo + { + public required object Instance { get; init; } + public required IEnumerable Properties { get; init; } + } + + internal class DebuggerDisplayResult + { + public required string Value { get; init; } + public required string Key { get; init; } + public required string Type { get; init; } + } + + internal static Type GetProxyType(object obj) + { + return GetProxyType(obj.GetType()); + } + + internal static Type GetProxyType(Type type) + { + var cad = FindAttribute(type, attributeType: typeof(DebuggerTypeProxyAttribute)); + + var proxyType = cad.ConstructorArguments[0].ArgumentType == typeof(Type) + ? (Type)cad.ConstructorArguments[0].Value! + : Type.GetType((string)cad.ConstructorArguments[0].Value!)!; + if (type.GenericTypeArguments.Length > 0) + { + proxyType = proxyType.MakeGenericType(type.GenericTypeArguments); + } + + return proxyType; + } + + internal static void CreateDebuggerTypeProxyWithNullArgument(Type type) + { + var proxyType = GetProxyType(type); + Activator.CreateInstance(proxyType, [null]); + } + + internal static string ValidateDebuggerDisplayReferences(object obj) + { + var cad = FindAttribute(obj.GetType(), attributeType: typeof(DebuggerDisplayAttribute)); + + // Get the text of the DebuggerDisplayAttribute + var attrText = (string)cad.ConstructorArguments[0].Value!; + + return EvaluateDisplayString(attrText, obj); + } + + internal static DebuggerAttributeInfo ValidateDebuggerTypeProxyProperties(object obj) + { + var proxyType = GetProxyType(obj); + + // Create an instance of the proxy type, and make sure we can access all of the instance properties + // on the type without exception + var proxyInstance = Activator.CreateInstance(proxyType, obj) ?? throw new InvalidOperationException(); + var properties = GetDebuggerVisibleProperties(proxyType); + return new DebuggerAttributeInfo + { + Instance = proxyInstance, + Properties = properties + }; + } + + internal static DebuggerBrowsableState? GetDebuggerBrowsableState(MemberInfo info) + { + var debuggerBrowsableAttribute = info.CustomAttributes + .SingleOrDefault(a => a.AttributeType == typeof(DebuggerBrowsableAttribute)); + // Enums in attribute constructors are boxed as ints, so cast to int? first. + return (DebuggerBrowsableState?)(int?)debuggerBrowsableAttribute?.ConstructorArguments.Single().Value; + } + + internal static IEnumerable GetDebuggerVisibleProperties(Type debuggerAttributeType) + { + // The debugger doesn't evaluate non-public members of type proxies. GetGetMethod returns null if the getter is non-public. + var visibleProperties = debuggerAttributeType.GetProperties() + .Where(pi => pi.GetGetMethod() != null && GetDebuggerBrowsableState(pi) != DebuggerBrowsableState.Never); + return visibleProperties; + } + + internal static DebuggerDisplayResult ValidateDebugViewValueListDictionaryItem(DebugViewValueListDictionaryItem obj) + { + var cad = FindAttribute(obj.GetType(), attributeType: typeof(DebuggerDisplayAttribute)); + + // Get the text of the DebuggerDisplayAttribute + var formattedValue = ValidateDebuggerDisplayReferences(obj.ValueList); + + var formattedKey = FormatDebuggerDisplayNamedArgument(nameof(DebuggerDisplayAttribute.Name), cad, obj); + var formattedType = FormatDebuggerDisplayNamedArgument(nameof(DebuggerDisplayAttribute.Type), cad, obj); + + + return new DebuggerDisplayResult { Value = formattedValue, Key = formattedKey, Type = formattedType }; + } + + private static string FormatDebuggerDisplayNamedArgument(string argumentName, CustomAttributeData debuggerDisplayAttributeData, object obj) + { + var namedAttribute = debuggerDisplayAttributeData.NamedArguments.FirstOrDefault(na => na.MemberName == argumentName); + if (namedAttribute != default) + { + var value = (string?)namedAttribute.TypedValue.Value; + if (!string.IsNullOrEmpty(value)) + return EvaluateDisplayString(value, obj); + } + return ""; + } + + + private static CustomAttributeData FindAttribute(Type type, Type attributeType) + { + for (var t = type; t != null; t = t.BaseType) + { + var attributes = t.GetTypeInfo().CustomAttributes + .Where(a => a.AttributeType == attributeType) + .ToArray(); + if (attributes.Length != 0) + return attributes.Length > 1 + ? throw new InvalidOperationException($"Expected one {attributeType.Name} on {type} but found more.") + : attributes[0]; + } + throw new InvalidOperationException($"Expected one {attributeType.Name} on {type}."); + } + + private static string EvaluateDisplayString(string displayString, object obj) + { + var objType = obj.GetType(); + var segments = displayString.Split('{', '}'); + + if (segments.Length % 2 == 0) + throw new InvalidOperationException($"The DebuggerDisplayAttribute for {objType} lacks a closing brace."); + + if (segments.Length == 1) + throw new InvalidOperationException($"The DebuggerDisplayAttribute for {objType} doesn't reference any expressions."); + + var sb = new StringBuilder(); + + for (var i = 0; i < segments.Length; i += 2) + { + var literal = segments[i]; + sb.Append(literal); + + if (i + 1 < segments.Length) + { + var reference = segments[i + 1]; + var noQuotes = reference.EndsWith(",nq"); + + reference = reference.Replace(",nq", string.Empty); + + // Evaluate the reference. + if (!TryEvaluateReference(obj, reference, out var member)) + throw new InvalidOperationException($"The DebuggerDisplayAttribute for {objType} contains the expression \"{reference}\"."); + + var memberString = GetDebuggerMemberString(member, noQuotes); + + sb.Append(memberString); + } + } + + return sb.ToString(); + } + + private static string GetDebuggerMemberString(object? member, bool noQuotes) + { + var memberString = "null"; + if (member != null) + { + memberString = member.ToString(); + if (member is string) + { + if (!noQuotes) + memberString = '"' + memberString + '"'; + } + else if (!IsPrimitiveType(member)) + memberString = '{' + memberString + '}'; + } + + return memberString!; + } + + private static bool IsPrimitiveType(object obj) => + obj is byte or sbyte or short or ushort or int or uint or long or ulong or float or double; + + private static bool TryEvaluateReference(object obj, string reference, out object? member) + { + var pi = GetProperty(obj, reference); + if (pi != null) + { + member = pi.GetValue(obj); + return true; + } + + var fi = GetField(obj, reference); + if (fi != null) + { + member = fi.GetValue(obj); + return true; + } + + member = null; + return false; + } + + private static FieldInfo? GetField(object obj, string fieldName) + { + for (var t = obj.GetType(); t != null; t = t.GetTypeInfo().BaseType) + { + var fi = t.GetTypeInfo().GetDeclaredField(fieldName); + if (fi != null) + return fi; + } + return null; + } + + private static PropertyInfo? GetProperty(object obj, string propertyName) + { + for (var t = obj.GetType(); t != null; t = t.GetTypeInfo().BaseType) + { + var pi = t.GetTypeInfo().GetDeclaredProperty(propertyName); + if (pi != null) + return pi; + } + return null; + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/FrugalListTest.cs b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTest.cs similarity index 89% rename from src/CommonUtilities/test/Collections/FrugalListTest.cs rename to src/CommonUtilities/test/Collections/FrugalList/FrugalListTest.cs index 264e320..0f32488 100644 --- a/src/CommonUtilities/test/Collections/FrugalListTest.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTest.cs @@ -1,7 +1,7 @@ using System; // ReSharper disable InconsistentNaming -namespace AnakinRaW.CommonUtilities.Test.Collections; +namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; public class FrugalListTest_String : FrugalListTestBase { diff --git a/src/CommonUtilities/test/Collections/FrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs similarity index 98% rename from src/CommonUtilities/test/Collections/FrugalListTestBase.cs rename to src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs index c58981f..4d0712a 100644 --- a/src/CommonUtilities/test/Collections/FrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs @@ -5,13 +5,9 @@ using AnakinRaW.CommonUtilities.Testing.Collections; using Xunit; -namespace AnakinRaW.CommonUtilities.Test.Collections; +namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; #pragma warning disable xUnit2013 - -/// -/// Contains tests that ensure the correctness of the class. -/// public abstract class FrugalListTestBase : IListTestSuite { protected override bool Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; diff --git a/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs similarity index 74% rename from src/CommonUtilities/test/Collections/ReadOnlyFrugalListTestBase.cs rename to src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs index 53313a0..cbf8235 100644 --- a/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs @@ -1,18 +1,15 @@ -using System; +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Collections; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing.Collections; using Xunit; -namespace AnakinRaW.CommonUtilities.Test.Collections; +namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; #pragma warning disable xUnit2013 -/// -/// Contains tests that ensure the correctness of the class. -/// [SuppressMessage("ReSharper", "InconsistentNaming")] public abstract class ReadOnlyFrugalListTestBase : IReadOnlyListTestSuite { @@ -169,6 +166,91 @@ public void CopyTo_ArrayIsLargerThanCollection(int count) #endregion + #region Contains + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Contains_ValidValueOnCollectionNotContainingThatValue(int count) + { + var collection = GenericReadOnlyListFrugalListFactory(count); + var seed = 4315; + var item = CreateT(seed++); + while (collection.Contains(item)) + item = CreateT(seed++); + Assert.False(collection.Contains(item)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Contains_ValidValueOnCollectionContainingThatValue(int count) + { + var collection = GenericReadOnlyListFrugalListFactory(count); + foreach (var item in collection) + Assert.True(collection.Contains(item)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Contains_DefaultValueOnCollectionNotContainingDefaultValue(int count) + { + var collection = GenericReadOnlyListFrugalListFactory(count); + if (default(T) is null) + Assert.False(collection.Contains(default!)); + } + + #endregion + + #region IndexOf + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IList_Generic_IndexOf_DefaultValueNotContainedInList(int count) + { + var list = GenericReadOnlyListFrugalListFactory(count); + var value = default(T); + if (list.Contains(value!)) + return; + Assert.Equal(-1, list.IndexOf(value!)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IList_Generic_IndexOf_DefaultValueContainedInList(int count) + { + if (count > 0) + { + var list = GenericReadOnlyListFrugalListFactory(count); + var value = default(T); + if (!list.Contains(value!)) + return; + Assert.Equal(0, list.IndexOf(value!)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IList_Generic_IndexOf_ValidValueNotContainedInList(int count) + { + var list = GenericReadOnlyListFrugalListFactory(count); + var seed = 54321; + var value = CreateT(seed++); + while (list.Contains(value)) + value = CreateT(seed++); + Assert.Equal(-1, list.IndexOf(value)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IList_Generic_IndexOf_EachValueNoDuplicates(int count) + { + // Assumes no duplicate elements contained in the list returned by GenericIListFactory + var list = GenericReadOnlyListFrugalListFactory(count); + foreach (var i in Enumerable.Range(0, count)) + Assert.Equal(i, list.IndexOf(list[i])); + } + + #endregion + #region Linq Equivalents [Theory] diff --git a/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTests.cs b/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTests.cs similarity index 50% rename from src/CommonUtilities/test/Collections/ReadOnlyFrugalListTests.cs rename to src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTests.cs index aca8c84..573bf82 100644 --- a/src/CommonUtilities/test/Collections/ReadOnlyFrugalListTests.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTests.cs @@ -1,9 +1,10 @@ -using System; +using AnakinRaW.CommonUtilities.Collections; +using System; using System.Collections.Generic; -using AnakinRaW.CommonUtilities.Collections; +using Xunit; // ReSharper disable InconsistentNaming -namespace AnakinRaW.CommonUtilities.Test.Collections; +namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; public class ReadOnlyFrugalListTest_String : ReadOnlyFrugalListTestBase { @@ -19,11 +20,37 @@ protected override string CreateT(int seed) public class ReadOnlyFrugalListTest_Int : ReadOnlyFrugalListTestBase { + private static readonly int[] _intArray = [-4, 5, -2, 3, 1, 2, -1, -3, 0, 4, -5, 3, 3]; + private static readonly int[] _excludedFromIntArray = [100, -34, 42, int.MaxValue, int.MinValue]; + protected override int CreateT(int seed) { var rand = new Random(seed); return rand.Next(); } + + [Fact] + public static void Contains() + { + var collection = new ReadOnlyFrugalList(_intArray); + foreach (var item in _intArray) + Assert.True(collection.Contains(item)); + + foreach (var excluded in _excludedFromIntArray) + Assert.False(collection.Contains(excluded)); + } + + [Fact] + public static void IndexOf() + { + var collection = new ReadOnlyFrugalList(_intArray); + + foreach (var item in _intArray) + Assert.Equal(Array.IndexOf(_intArray, item), collection.IndexOf(item)); + + foreach (var excluded in _excludedFromIntArray) + Assert.Equal(-1, collection.IndexOf(excluded)); + } } diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs new file mode 100644 index 0000000..b05b1b1 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs @@ -0,0 +1,743 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. + +public abstract class IReadOnlyValueListDictionaryTestBase : IEnumerableTestSuite>> + where TKey : notnull +{ + protected abstract bool DefaultValueAllowed { get; } + + protected virtual bool IsReadOnly => true; + + protected override bool Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + + protected override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; + + protected override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; + + protected abstract TKey CreateTKey(int seed); + + protected abstract TValue CreateTValue(int seed); + + protected abstract IReadOnlyValueListDictionary IReadOnlyValueListDictionaryFactory(int count); + + protected override IEnumerable>> GenericIEnumerableFactory(int count) + { + return IReadOnlyValueListDictionaryFactory(count); + } + + protected override IEqualityComparer>> GetIEqualityComparer() + { + return new KVPComparer(); + } + + protected TKey GetNewKey(IReadOnlyValueListDictionary dictionary) + { + var seed = 840; + var missingKey = CreateTKey(seed++); + while (dictionary.ContainsKey(missingKey) || missingKey.Equals(default(TKey))) + missingKey = CreateTKey(seed++); + return missingKey; + } + + protected void AddToCollection(IValueListDictionary dictionary, int numberOfItemsToAdd) + { + var seed = 12353; + var random = new Random(); + var initialCount = dictionary.KeyCount; + while (dictionary.KeyCount - initialCount < numberOfItemsToAdd) + { + var toAdd = CreateTKey(seed++); + while (dictionary.ContainsKey(toAdd)) + toAdd = CreateTKey(seed++); + + dictionary.Add(toAdd, CreateTValue(seed++)); + while (random.Next() % 2 == 0) + dictionary.Add(toAdd, CreateTValue(seed++)); + } + } + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) + { + // ReSharper disable UseMethodAny.0 + if (IsReadOnly) + yield break; + + if ((operations & ModifyOperation.Add) == ModifyOperation.Add) + { + yield return enumerable => + { + var casted = (IValueListDictionary)enumerable; + casted.Add(CreateTKey(12), CreateTValue(5123)); + return true; + }; + } + if ((operations & ModifyOperation.Insert) == ModifyOperation.Insert) + { + yield return enumerable => + { + var casted = (IValueListDictionary)enumerable; + casted.Add(CreateTKey(541), CreateTValue(12)); + return true; + }; + } + if ((operations & ModifyOperation.Remove) == ModifyOperation.Remove) + { + yield return enumerable => + { + var casted = (IValueListDictionary)enumerable; + if (casted.Count() > 0) + { + using var keys = casted.Keys.GetEnumerator(); + keys.MoveNext(); + casted.Remove(keys.Current!); + return true; + } + return false; + }; + } + if ((operations & ModifyOperation.Clear) == ModifyOperation.Clear) + { + yield return enumerable => + { + var casted = (IValueListDictionary)enumerable; + if (casted.Count() > 0) + { + casted.Clear(); + return true; + } + return false; + }; + } + //throw new InvalidOperationException(string.Format("{0:G}", operations)); + // ReSharper restore UseMethodAny.0 + } + + #region Item Getter + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ItemGet_DefaultKey(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary[default!]); + return; + } + + if (!IsReadOnly) + { + var value = CreateTValue(3452); + AddValue(dictionary, default!, value); + Assert.Equal(value, dictionary[default!].First()); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ItemGet_MissingNonDefaultKey_ThrowsKeyNotFoundException(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary[missingKey]); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ItemGet_MissingDefaultKey_ThrowsKeyNotFoundException(int count) + { + if (DefaultValueAllowed && !IsReadOnly) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + RemoveKey(dictionary, missingKey); + Assert.Throws(() => dictionary[missingKey]); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ItemGet_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + foreach (var pair in dictionary) + Assert.Equal(pair.Value, dictionary[pair.Key]); + } + + #endregion + + #region Keys + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Keys_ContainsAllCorrectKeys(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var expected = dictionary.Select(pair => pair.Key); + Assert.True(expected.SequenceEqual(dictionary.Keys)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Keys_IsReadOnly(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var keys = dictionary.Keys; + Assert.True(keys.IsReadOnly); + Assert.Throws(() => keys.Add(CreateTKey(11))); + Assert.Throws(() => keys.Clear()); + Assert.Throws(() => keys.Remove(CreateTKey(11))); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Keys_Enumeration_Reset(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var keys = dictionary.Keys; + using var enumerator = keys.GetEnumerator(); + enumerator.Reset(); + } + + #endregion + + #region Values + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Values_ContainsAllCorrectFlattenedValues(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var expected = dictionary.SelectMany(pair => pair.Value); + Assert.True(expected.SequenceEqual(dictionary.Values)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Values_IsReadOnly(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var values = dictionary.Values; + Assert.True(values.IsReadOnly); + Assert.Throws(() => values.Add(CreateTValue(11))); + Assert.Throws(() => values.Clear()); + Assert.Throws(() => values.Remove(CreateTValue(11))); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Values_Enumeration_Reset(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var values = dictionary.Values; + using var enumerator = values.GetEnumerator(); + enumerator.Reset(); + } + + #endregion + + #region Count + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Count_Validity(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var expectedCount = dictionary.Sum(pair => pair.Value.Count); + Assert.Equal(expectedCount, dictionary.Count); + } + + #endregion + + #region KeyCount + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void KeyCount_Validity(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + Assert.Equal(count, dictionary.KeyCount); + } + + #endregion + + #region ContainsKey + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_ContainsKey_ValidKeyNotContainedInDictionary(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.False(dictionary.ContainsKey(missingKey)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_ContainsKey_ValidKeyContainedInDictionary(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (count > 0) + { + var key = dictionary.Keys.First(); + Assert.True(dictionary.ContainsKey(key)); + } + + if (!IsReadOnly) + { + var missingKey = GetNewKey(dictionary); + AddValue(dictionary, missingKey, CreateTValue(34251)); + Assert.True(dictionary.ContainsKey(missingKey)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_ContainsKey_DefaultKeyNotContainedInDictionary(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (DefaultValueAllowed) + { + if (!IsReadOnly) + { + // returns false + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + RemoveKey(dictionary, missingKey); + Assert.False(dictionary.ContainsKey(missingKey)); + } + } + else + { + // throws ArgumentNullException + Assert.Throws(() => dictionary.ContainsKey(default!)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_ContainsKey_DefaultKeyContainedInDictionary(int count) + { + if (DefaultValueAllowed && !IsReadOnly) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + if (!dictionary.ContainsKey(missingKey)) + AddValue(dictionary, missingKey, CreateTValue(5341)); + Assert.True(dictionary.ContainsKey(missingKey)); + } + } + + #endregion + + #region GetValues + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetValues_DefaultKey(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.GetValues(default!)); + return; + } + + if (!IsReadOnly) + { + var value = CreateTValue(3452); + AddValue(dictionary, default!, value); + Assert.Equal(value, dictionary.GetValues(default!).First()); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetValues_MissingNonDefaultKey_ThrowsKeyNotFoundException(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary.GetValues(missingKey)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetValues_MissingDefaultKey_ThrowsKeyNotFoundException(int count) + { + if (DefaultValueAllowed && !IsReadOnly) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + RemoveKey(dictionary, missingKey); + Assert.Throws(() => dictionary.GetValues(missingKey)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetValues_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + foreach (var pair in dictionary) + Assert.Equal(pair.Value, dictionary.GetValues(pair.Key)); + } + + #endregion + + #region GetFirstValue + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetFirstValue_DefaultKey(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.GetFirstValue(default!)); + return; + } + + if (!IsReadOnly) + { + var first = CreateTValue(3452); + var second = CreateTValue(4312); + AddValue(dictionary, default!, first); + AddValue(dictionary, default!, second); + Assert.Equal(first, dictionary.GetFirstValue(default!)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetFirstValue_MissingNonDefaultKey_ThrowsKeyNotFoundException(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary.GetFirstValue(missingKey)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetFirstValue_MissingDefaultKey_ThrowsKeyNotFoundException(int count) + { + if (DefaultValueAllowed && !IsReadOnly) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + RemoveKey(dictionary, missingKey); + Assert.Throws(() => dictionary.GetFirstValue(missingKey)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetFirstValue_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + foreach (var pair in dictionary) + Assert.Equal(pair.Value.First(), dictionary.GetFirstValue(pair.Key)); + } + + #endregion + + #region GetLastValue + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetLastValue_DefaultKey(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.GetLastValue(default!)); + return; + } + + if (!IsReadOnly) + { + var first = CreateTValue(3452); + var second = CreateTValue(4312); + AddValue(dictionary, default!, first); + AddValue(dictionary, default!, second); + Assert.Equal(second, dictionary.GetLastValue(default!)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetLastValue_MissingNonDefaultKey_ThrowsKeyNotFoundException(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary.GetLastValue(missingKey)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetLastValue_MissingDefaultKey_ThrowsKeyNotFoundException(int count) + { + if (DefaultValueAllowed && !IsReadOnly) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + RemoveKey(dictionary, missingKey); + Assert.Throws(() => dictionary.GetLastValue(missingKey)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetLastValue_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + foreach (var pair in dictionary) + Assert.Equal(pair.Value.Last(), dictionary.GetLastValue(pair.Key)); + } + + #endregion + + #region TryGetValues + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetValues_DefaultKey(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.TryGetValues(default!, out _)); + return; + } + + if (!IsReadOnly) + { + var first = CreateTValue(3452); + var second = CreateTValue(5431); + AddValue(dictionary, default!, first); + AddValue(dictionary, default!, second); + Assert.True(dictionary.TryGetValues(default!, out var valueList)); + Assert.Equal([first, second], valueList); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetValues_MissingNonDefaultKey_ReturnsFalseAndSetsDefault(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.False(dictionary.TryGetValues(missingKey, out var valueList)); + Assert.Equal(default, valueList); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetValues_MissingDefaultKey_ReturnsFalseAndSetsDefault(int count) + { + if (DefaultValueAllowed && !IsReadOnly) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + RemoveKey(dictionary, missingKey); + Assert.False(dictionary.TryGetValues(missingKey, out var valueList)); + Assert.Equal(default, valueList); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetValues_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + foreach (var pair in dictionary) + { + Assert.True(dictionary.TryGetValues(pair.Key, out var valueList)); + Assert.Equal(pair.Value, valueList); + } + } + + #endregion + + #region TryGetFirstValue + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetFirstValue_DefaultKey(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.TryGetFirstValue(default!, out _)); + return; + } + + if (!IsReadOnly) + { + var first = CreateTValue(3452); + var second = CreateTValue(4312); + AddValue(dictionary, default!, first); + AddValue(dictionary, default!, second); + + Assert.True(dictionary.TryGetFirstValue(default!, out var value)); + Assert.Equal(first, value); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetFirstValue_MissingNonDefaultKey_ReturnsFalseAndSetsDefaultValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.False(dictionary.TryGetFirstValue(missingKey, out var value)); + Assert.Equal(default, value); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetFirstValue_MissingDefaultKey_ReturnsFalseAndSetsDefaultValue(int count) + { + if (DefaultValueAllowed && !IsReadOnly) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + RemoveKey(dictionary, missingKey); + Assert.False(dictionary.TryGetFirstValue(missingKey, out var value)); + Assert.Equal(default, value); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetFirstValue_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + foreach (var pair in dictionary) + { + Assert.True(dictionary.TryGetFirstValue(pair.Key, out var value)); + Assert.Equal(pair.Value.First(), value); + } + } + + #endregion + + #region TryGetLastValue + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetLastValue_DefaultKey(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.TryGetLastValue(default!, out _)); + return; + } + + if (!IsReadOnly) + { + var first = CreateTValue(3452); + var second = CreateTValue(4312); + AddValue(dictionary, default!, first); + AddValue(dictionary, default!, second); + Assert.True(dictionary.TryGetLastValue(default!, out var value)); + Assert.Equal(second, value); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetLastValue_MissingNonDefaultKey_ReturnsFalseAndSetsDefaultValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.False(dictionary.TryGetLastValue(missingKey, out var value)); + Assert.Equal(default, value); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetLastValue_MissingDefaultKey_ReturnsFalseAndSetsDefaultValue(int count) + { + if (DefaultValueAllowed && !IsReadOnly) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + RemoveKey(dictionary, missingKey); + Assert.False(dictionary.TryGetLastValue(missingKey, out var value)); + Assert.Equal(default, value); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void TryGetLastValue_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = IReadOnlyValueListDictionaryFactory(count); + foreach (var pair in dictionary) + { + Assert.True(dictionary.TryGetLastValue(pair.Key, out var value)); + Assert.Equal(pair.Value.Last(), value); + } + } + + #endregion + + private void RemoveKey(IReadOnlyValueListDictionary dictionary, TKey key) + { + if (IsReadOnly) + throw new NotSupportedException("Test is read-only."); + if (dictionary is not IValueListDictionary mutable) + throw new InvalidOperationException("Could not cast to mutable version"); + mutable.Remove(key); + } + + private void AddValue(IReadOnlyValueListDictionary dictionary, TKey key, TValue value) + { + if (IsReadOnly) + throw new NotSupportedException("Test is read-only."); + if (dictionary is not IValueListDictionary mutable) + throw new InvalidOperationException("Could not cast to mutable version"); + + mutable.Add(key, value); + } + + // ReSharper disable once InconsistentNaming + public class KVPComparer : IEqualityComparer>> + { + public bool Equals(KeyValuePair> x, KeyValuePair> y) + { + if (!Equals(x.Key, y.Key)) + return false; + + if (x.Value.Count != y.Value.Count) + return false; + return !x.Value.Where((t, i) => !Equals(t, y.Value[i])).Any(); + } + + public int GetHashCode(KeyValuePair> obj) + { + var hashCode = new HashCode(); + + hashCode.Add(obj.Key); + foreach (var item in obj.Value) + hashCode.Add(item); + return hashCode.ToHashCode(); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs new file mode 100644 index 0000000..fbbd86f --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs @@ -0,0 +1,430 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +public abstract class IValueListDictionaryTestBase : IReadOnlyValueListDictionaryTestBase + where TKey : notnull +{ + protected override bool IsReadOnly => false; + + protected abstract IValueListDictionary IValueListDictionaryFactory(IEqualityComparer? comparer = null); + + protected virtual IValueListDictionary IValueListDictionaryFactory(int count) + { + var collection = IValueListDictionaryFactory(); + AddToCollection(collection, count); + return collection; + } + + protected override IReadOnlyValueListDictionary IReadOnlyValueListDictionaryFactory(int count) + { + return IValueListDictionaryFactory(count); + } + + #region Keys + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Keys_ModifyingTheDictionaryUpdatesTheCollection(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var keys = dictionary.Keys; + if (count > 0) + Assert.NotEmpty(keys); + dictionary.Clear(); + Assert.Empty(keys); + + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Keys_Enumeration_ParentDictionaryModifiedInvalidates(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var keys = dictionary.Keys; + using var keysEnum = keys.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + + Assert.Throws(() => keysEnum.MoveNext()); + Assert.Throws(() => keysEnum.Reset()); + } + + #endregion + + #region Values + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Values_Enumeration_ParentDictionaryModifiedInvalidates(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var values = dictionary.Values; + using var valuesEnum = values.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (valuesEnum.MoveNext()) + { + _ = valuesEnum.Current; + } + valuesEnum.Reset(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Values_IncludeDuplicatesMultipleTimes(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var oldValueCount = dictionary.Count; + var oldKeyCount = dictionary.KeyCount; + var seed = 431; + foreach (var pair in dictionary.ToList()) + { + var missingKey = CreateTKey(seed++); + while (dictionary.ContainsKey(missingKey)) + missingKey = CreateTKey(seed++); + dictionary.Add(missingKey, pair.Value.First()); + } + Assert.Equal(oldValueCount + oldKeyCount, dictionary.Values.Count); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Values_ModifyingTheDictionaryUpdatesTheCollection(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var values = dictionary.Values; + if (count > 0) + Assert.NotEmpty(values); + + dictionary.Clear(); + Assert.Empty(values); + } + + #endregion + + #region Add(TKey, TValue) + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Add_DefaultKey_DefaultValue(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var valueCoutBeforeAdd = dictionary.Count; + var missingKey = default(TKey)!; + var value = default(TValue)!; + if (DefaultValueAllowed) + { + Assert.False(dictionary.Add(missingKey, value)); + Assert.Equal(valueCoutBeforeAdd + 1, dictionary.Count); + Assert.Equal(count + 1, dictionary.KeyCount); + Assert.Equal(value, dictionary[missingKey].First()); + Assert.Equal(value, dictionary[missingKey].Last()); + } + else + { + Assert.Throws(() => dictionary.Add(missingKey, value)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Add_DefaultKey_NonDefaultValue(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var valueCoutBeforeAdd = dictionary.Count; + var missingKey = default(TKey)!; + var value = CreateTValue(1456); + if (DefaultValueAllowed) + { + Assert.False(dictionary.Add(missingKey, value)); + Assert.Equal(valueCoutBeforeAdd + 1, dictionary.Count); + Assert.Equal(count + 1, dictionary.KeyCount); + Assert.Equal(value, dictionary[missingKey].First()); + Assert.Equal(value, dictionary[missingKey].Last()); + } + else + { + Assert.Throws(() => dictionary.Add(missingKey, value)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Add_NonDefaultKey_DefaultValue(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var valueCoutBeforeAdd = dictionary.Count; + var missingKey = GetNewKey(dictionary); + var value = default(TValue)!; + Assert.False(dictionary.Add(missingKey, value)); + Assert.Equal(valueCoutBeforeAdd + 1, dictionary.Count); + Assert.Equal(count + 1, dictionary.KeyCount); + Assert.Equal(value, dictionary[missingKey].First()); + Assert.Equal(value, dictionary[missingKey].Last()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Add_NonDefaultKey_NonDefaultValue(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var valueCoutBeforeAdd = dictionary.Count; + var missingKey = GetNewKey(dictionary); + var value = CreateTValue(1342); + Assert.False(dictionary.Add(missingKey, value)); + Assert.Equal(valueCoutBeforeAdd + 1, dictionary.Count); + Assert.Equal(count + 1, dictionary.KeyCount); + Assert.Equal(value, dictionary[missingKey].First()); + Assert.Equal(value, dictionary[missingKey].Last()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Add_DuplicateValue(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var seed = 321; + var duplicate = CreateTValue(seed++); + while (dictionary.Values.Contains(duplicate)) + duplicate = CreateTValue(seed++); + Assert.False(dictionary.Add(GetNewKey(dictionary), duplicate)); + Assert.False(dictionary.Add(GetNewKey(dictionary), duplicate)); + Assert.Equal(2, dictionary.Values.Count(value => value!.Equals(duplicate))); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Add_DuplicateKey_AddsToList(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var duplicateKey = GetNewKey(dictionary); + Assert.False(dictionary.Add(duplicateKey, CreateTValue(34251))); + Assert.Single(dictionary[duplicateKey]); + var valueCountBeforeSecondAdd = dictionary.Count; + var keyCountBeforeSecondAdd = dictionary.KeyCount; + + Assert.True(dictionary.Add(duplicateKey, CreateTValue(134))); + Assert.Equal(2, dictionary[duplicateKey].Count); + Assert.Equal(keyCountBeforeSecondAdd, dictionary.KeyCount); + Assert.Equal(valueCountBeforeSecondAdd + 1, dictionary.Count); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Add_DistinctValuesWithHashCollisions(int count) + { + var dictionary = IValueListDictionaryFactory(new EqualityComparerConstantHashCode(EqualityComparer.Default)); + AddToCollection(dictionary, count); + Assert.Equal(count, dictionary.KeyCount); + } + + #endregion + + #region Remove(TKey) + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Remove_EveryKey(int count) + { + var dictionary = IValueListDictionaryFactory(count); + Assert.All(dictionary.Keys.ToList(), key => + { + Assert.True(dictionary.Remove(key)); + }); + Assert.Empty(dictionary); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Remove_ValidKeyNotContainedInDictionary(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.False(dictionary.Remove(missingKey)); + Assert.Equal(count, dictionary.KeyCount); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Remove_ValidKeyContainedInDictionary(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + dictionary.Add(missingKey, CreateTValue(34251)); + Assert.True(dictionary.Remove(missingKey)); + Assert.Equal(count, dictionary.KeyCount); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Remove_DefaultKeyNotContainedInDictionary(int count) + { + var dictionary = IValueListDictionaryFactory(count); + if (DefaultValueAllowed) + { + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + dictionary.Remove(missingKey); + Assert.False(dictionary.Remove(missingKey)); + } + else + { + Assert.Throws(() => dictionary.Remove(default!)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_Remove_DefaultKeyContainedInDictionary(int count) + { + if (DefaultValueAllowed) + { + var dictionary = IValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + dictionary.Add(missingKey, CreateTValue(5341)); + Assert.True(dictionary.Remove(missingKey)); + } + } + + #endregion + + #region Remove(TKey, TValue) + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_RemoveKeyValue_Everything(int count) + { + var dictionary = IValueListDictionaryFactory(count); + Assert.All(dictionary.Keys.ToList(), key => + { + foreach (var value in dictionary.GetValues(key)) + Assert.True(dictionary.Remove(key, value)); + }); + Assert.Empty(dictionary); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_RemoveKeyValue_ValidKeyNotContainedInDictionary(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.False(dictionary.Remove(missingKey, default!)); + Assert.Equal(count, dictionary.KeyCount); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + var value = CreateTValue(34251); + dictionary.Add(missingKey, value); + Assert.True(dictionary.Remove(missingKey, value)); + Assert.Equal(count, dictionary.KeyCount); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary_ValueNotContained(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + var seed = 34251; + var value = CreateTValue(seed++)!; + + var missingValue = CreateTValue(seed++); + while (value.Equals(missingValue)) + missingValue = CreateTValue(seed++); + + dictionary.Add(missingKey, value); + Assert.False(dictionary.Remove(missingKey, missingValue)); + Assert.Equal(count + 1, dictionary.KeyCount); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary_DuplicateValues(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + var value = CreateTValue(34251); + + dictionary.Add(missingKey, value); + dictionary.Add(missingKey, value); + + Assert.True(dictionary.Remove(missingKey, value)); + Assert.Equal([value], dictionary.GetValues(missingKey)); + Assert.Equal(count + 1, dictionary.KeyCount); + } + + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_RemoveKeyValue_DefaultKeyNotContainedInDictionary(int count) + { + var dictionary = IValueListDictionaryFactory(count); + if (DefaultValueAllowed) + { + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + dictionary.Remove(missingKey); + Assert.False(dictionary.Remove(missingKey, default!)); + } + else + { + Assert.Throws(() => dictionary.Remove(default!, default!)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_Generic_RemoveKeyValue_DefaultKeyContainedInDictionary(int count) + { + if (DefaultValueAllowed) + { + var dictionary = IValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + var value = CreateTValue(5341); + dictionary.Add(missingKey, value); + Assert.True(dictionary.Remove(missingKey, value)); + } + } + + #endregion + + #region Clear + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ICollection_Generic_Clear(int count) + { + var dictionary = IValueListDictionaryFactory(count); + dictionary.Clear(); + Assert.Equal(0, dictionary.Count); + Assert.Equal(0, dictionary.KeyCount); + Assert.Empty(dictionary.Keys); + Assert.Empty(dictionary.Values); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ICollection_Generic_Clear_Repeatedly(int count) + { + var dictionary = IValueListDictionaryFactory(count); + dictionary.Clear(); + dictionary.Clear(); + dictionary.Clear(); + Assert.Equal(0, dictionary.Count); + Assert.Equal(0, dictionary.KeyCount); + Assert.Empty(dictionary.Keys); + Assert.Empty(dictionary.Values); + } + + #endregion +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Keys.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Keys.cs new file mode 100644 index 0000000..a9db5a9 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Keys.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +// ReSharper disable once InconsistentNaming +public class ReadOnlyValueListDictionary_Keys : ICollectionTestSuite +{ + protected override bool Enumerator_Empty_UsesSingletonInstance => false; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => false; + protected override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; + protected override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override bool DefaultValueAllowed => false; + protected override bool DuplicateValuesAllowed => false; + protected override bool IsReadOnly => true; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + + protected override ICollection GenericICollectionFactory() + { + return new ReadOnlyValueListDictionary(new ValueListDictionary()).Keys; + } + + protected override ICollection GenericICollectionFactory(int count) + { + var list = new ValueListDictionary(); + var seed = 13453; + var random = new Random(); + for (var i = 0; i < count; i++) + { + var key = CreateT(seed++); + list.Add(key, CreateT(seed++)); + while (random.Next() % 2 == 0) + list.Add(key, CreateT(seed++)); + } + return new ReadOnlyValueListDictionary(list).Keys; + } + + protected override string CreateT(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + [Fact] + public void ValueListDictionary_KeyCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ReadOnlyValueListDictionary.KeyCollection(null!)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ValueListDictionary_KeyCollection_GetEnumerator(int count) + { + var dictionary = new ValueListDictionary(); + var seed = 13453; + while (dictionary.Count < count) + dictionary.Add(CreateT(seed++), CreateT(seed++)); + using var _ = new ReadOnlyValueListDictionary(dictionary).Keys.GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Values.cs new file mode 100644 index 0000000..6032610 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Values.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +// ReSharper disable once InconsistentNaming +public class ReadOnlyValueListDictionary_Values : ICollectionTestSuite +{ + protected override bool DefaultValueAllowed => true; + protected override bool DuplicateValuesAllowed => true; + protected override bool IsReadOnly => true; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + protected override bool Enumerator_Empty_UsesSingletonInstance => false; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => false; + + protected override ICollection GenericICollectionFactory() + { + return new ReadOnlyValueListDictionary(new ValueListDictionary()).Values; + } + + protected override ICollection GenericICollectionFactory(int count) + { + var list = new ValueListDictionary(); + var seed = 13453; + var random = new Random(seed); + + var valuesAdded = 0; + while (valuesAdded < count) + { + var key = CreateT(seed++); + + // Add first value for this key + list.Add(key, CreateT(seed++)); + valuesAdded++; + + // Randomly add more values for the same key, but don't exceed count + while (valuesAdded < count && random.Next() % 2 == 0) + { + list.Add(key, CreateT(seed++)); + valuesAdded++; + } + } + + return new ReadOnlyValueListDictionary(list).Values; + } + + protected override string CreateT(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + [Fact] + public void ValueListDictionary_ValueCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ReadOnlyValueListDictionary.ValueCollection(null!)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ValueListDictionary_ValueCollection_GetEnumerator(int count) + { + var dictionary = new ValueListDictionary(); + var seed = 13453; + while (dictionary.Count < count) + dictionary.Add(CreateT(seed++), CreateT(seed++)); + using var _ = new ReadOnlyValueListDictionary(dictionary).Values.GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs new file mode 100644 index 0000000..69aaa70 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs @@ -0,0 +1,94 @@ +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing; +using System; +using System.Collections.Generic; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +public abstract class ReadOnlyValueListDictionaryTestBase : IReadOnlyValueListDictionaryTestBase + where TKey : notnull +{ + protected override bool DefaultValueAllowed => false; + + protected override bool IsReadOnly => true; + + protected override KeyValuePair> CreateT(int seed) + { + throw new NotSupportedException(); + } + + protected virtual ReadOnlyValueListDictionary ReadOnlyValueListDictionaryFactory(int count) + { + var collection = new ValueListDictionary(); + AddToCollection(collection, count); + return new ReadOnlyValueListDictionary(collection); + } + + protected override IReadOnlyValueListDictionary IReadOnlyValueListDictionaryFactory(int count) + { + return ReadOnlyValueListDictionaryFactory(count); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void CtorTests(int count) + { + var collection = new ValueListDictionary(); + AddToCollection(collection, count); + var readOnlyDictionary = new ReadOnlyValueListDictionary(collection); + + Assert.Equal(collection.KeyCount, readOnlyDictionary.KeyCount); + Assert.Equal(collection.Count, readOnlyDictionary.Count); + } + + [Fact] + public static void CtorTests_Negative() + { + AssertExtensions.Throws("dictionary", () => _ = new ReadOnlyValueListDictionary(null!)); + } + + [Fact] + public static void Empty_Idempotent() + { + Assert.NotNull(ReadOnlyValueListDictionary.Empty); + Assert.Equal(0, ReadOnlyValueListDictionary.Empty.Count); + Assert.Same(ReadOnlyValueListDictionary.Empty, ReadOnlyValueListDictionary.Empty); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void EnumeratorTest(int count) + { + var collection = new ValueListDictionary(); + AddToCollection(collection, count); + var readOnlyDictionary = new ReadOnlyValueListDictionary(collection); + + using var enumerator = readOnlyDictionary.GetEnumerator(); + foreach (var keyValuePair in collection) + { + Assert.True(enumerator.MoveNext()); + + Assert.Equal(keyValuePair.Key, enumerator.Current.Key); + Assert.Equal(keyValuePair.Value, enumerator.Current.Value); + } + Assert.False(enumerator.MoveNext()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void SourceModificationsReflectedInReadOnlyDictionary(int count) + { + var collection = new ValueListDictionary(); + AddToCollection(collection, count); + var readOnlyDictionary = new ReadOnlyValueListDictionary(collection); + + Assert.Equal(collection.KeyCount, readOnlyDictionary.KeyCount); + Assert.Equal(collection.Count, readOnlyDictionary.Count); + + collection.Add(GetNewKey(collection), CreateTValue(4231)); + + Assert.Equal(collection.KeyCount, readOnlyDictionary.KeyCount); + Assert.Equal(collection.Count, readOnlyDictionary.Count); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTests.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTests.cs new file mode 100644 index 0000000..f27d170 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTests.cs @@ -0,0 +1,40 @@ +using System; +// ReSharper disable InconsistentNaming + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +public class ReadOnlyValueListDictionaryTest_string_string : ReadOnlyValueListDictionaryTestBase +{ + protected override bool DefaultValueAllowed => false; + + protected override string CreateTKey(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + + protected override string CreateTValue(int seed) + { + return CreateTKey(seed); + } +} + +public class ReadOnlyValueListDictionaryTest_int_int : ReadOnlyValueListDictionaryTestBase +{ + protected override bool DefaultValueAllowed => true; + + protected override int CreateTKey(int seed) + { + var rand = new Random(seed); + return rand.Next(); + } + + protected override int CreateTValue(int seed) + { + return CreateTKey(seed); + } +} + diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Keys.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Keys.cs new file mode 100644 index 0000000..08dc4e4 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Keys.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +// ReSharper disable once InconsistentNaming +public class ValueListDictionary_Keys : ICollectionTestSuite +{ + protected override bool Enumerator_Empty_UsesSingletonInstance => false; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => false; + protected override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; + protected override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override bool DefaultValueAllowed => false; + protected override bool DuplicateValuesAllowed => false; + protected override bool IsReadOnly => true; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + + protected override ICollection GenericICollectionFactory() + { + return new ValueListDictionary().Keys; + } + + protected override ICollection GenericICollectionFactory(int count) + { + var list = new ValueListDictionary(); + var seed = 13453; + var random = new Random(); + for (var i = 0; i < count; i++) + { + var key = CreateT(seed++); + list.Add(key, CreateT(seed++)); + while (random.Next() % 2 == 0) + list.Add(key, CreateT(seed++)); + } + return list.Keys; + } + + protected override string CreateT(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + [Fact] + public void ValueListDictionary_KeyCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ValueListDictionary.KeyCollection(null!)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ValueListDictionary_KeyCollection_GetEnumerator(int count) + { + var dictionary = new ValueListDictionary(); + var seed = 13453; + while (dictionary.Count < count) + dictionary.Add(CreateT(seed++), CreateT(seed++)); + using var _ = dictionary.Keys.GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Values.cs new file mode 100644 index 0000000..ee9cf0d --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Values.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +// ReSharper disable once InconsistentNaming +public class ValueListDictionary_Values : ICollectionTestSuite +{ + protected override bool DefaultValueAllowed => true; + protected override bool DuplicateValuesAllowed => true; + protected override bool IsReadOnly => true; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + protected override bool Enumerator_Empty_UsesSingletonInstance => false; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => false; + + protected override ICollection GenericICollectionFactory() + { + return new ValueListDictionary().Values; + } + + protected override ICollection GenericICollectionFactory(int count) + { + var list = new ValueListDictionary(); + var seed = 13453; + var random = new Random(seed); + + var valuesAdded = 0; + while (valuesAdded < count) + { + var key = CreateT(seed++); + + // Add first value for this key + list.Add(key, CreateT(seed++)); + valuesAdded++; + + // Randomly add more values for the same key, but don't exceed count + while (valuesAdded < count && random.Next() % 2 == 0) + { + list.Add(key, CreateT(seed++)); + valuesAdded++; + } + } + + return list.Values; + } + + protected override string CreateT(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + [Fact] + public void ValueListDictionary_ValueCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ValueListDictionary.ValueCollection(null!)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ValueListDictionary_ValueCollection_GetEnumerator(int count) + { + var dictionary = new ValueListDictionary(); + var seed = 13453; + while (dictionary.Count < count) + dictionary.Add(CreateT(seed++), CreateT(seed++)); + using var _ = dictionary.Values.GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestBase.cs new file mode 100644 index 0000000..8f6c02a --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestBase.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +public abstract class ValueListDictionaryTestBase : IValueListDictionaryTestBase where TKey : notnull +{ + protected override bool DefaultValueAllowed => false; + + protected override KeyValuePair> CreateT(int seed) + { + throw new NotSupportedException(); + } + + protected override IValueListDictionary IValueListDictionaryFactory(IEqualityComparer? comparer = null) + { + return ValueListDictionaryFactory(comparer); + } + + protected override IValueListDictionary IValueListDictionaryFactory(int count) + { + return ValueListDictionaryFactory(count); + } + + protected ValueListDictionary ValueListDictionaryFactory(IEqualityComparer? comparer = null) + { + return new ValueListDictionary(comparer); + } + + protected virtual ValueListDictionary ValueListDictionaryFactory(int count) + { + var collection = ValueListDictionaryFactory(); + AddToCollection(collection, count); + return collection; + } + + #region Constructors + + [Fact] + public void Dictionary_CapacityAtLeastPassedValue() + { + var dict = new ValueListDictionary(); + Assert.Equal(0, dict.Count); + Assert.Equal(0, dict.KeyCount); + } + + #endregion + + #region IReadOnlyValueListDictionary.Keys & Values + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IReadOnlyValueListDictionary_Keys_ContainsAllCorrectKeys(int count) + { + var dictionary = ValueListDictionaryFactory(count); + var expected = dictionary.Select(pair => pair.Key); + IEnumerable keys = ((IReadOnlyValueListDictionary)dictionary).Keys; + Assert.True(expected.SequenceEqual(keys)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IReadOnlyValueListDictionary_Values_ContainsAllCorrectValues(int count) + { + var dictionary = ValueListDictionaryFactory(count); + var expected = dictionary.SelectMany(pair => pair.Value); + IEnumerable values = ((IReadOnlyValueListDictionary)dictionary).Values; + Assert.True(expected.SequenceEqual(values)); + } + + #endregion +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTests.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTests.cs new file mode 100644 index 0000000..a30046c --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTests.cs @@ -0,0 +1,37 @@ +using System; +// ReSharper disable InconsistentNaming + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +public class ValueListDictionaryTest_string_string : ValueListDictionaryTestBase +{ + protected override string CreateTKey(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + + protected override string CreateTValue(int seed) + { + return CreateTKey(seed); + } +} + +public class ValueListDictionaryTest_int_int : ValueListDictionaryTestBase +{ + protected override bool DefaultValueAllowed => true; + + protected override int CreateTKey(int seed) + { + var rand = new Random(seed); + return rand.Next(); + } + + protected override int CreateTValue(int seed) + { + return CreateTKey(seed); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index 39eb3bd..a481d78 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -11,18 +11,26 @@ AnakinRaW.CommonUtilities.Test AnakinRaW.CommonUtilities.Test true + enable - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -31,6 +39,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From e4cebe58647c8ef57b712c7ec0b35539b339003f Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 28 Dec 2025 14:44:14 +0100 Subject: [PATCH 29/43] Public Testing (#292) * reorganize project files * rename project file * move files * switch to xunit 3 * new test classes * resolve some warnings * rename tests * resolve warnings * resolve more warnings * more warnigns * code doc --- .github/workflows/test.yml | 2 +- CommonUtilities.slnx | 2 +- Directory.Build.props | 10 +- global.json | 5 + .../CommonUtilities.DownloadManager.csproj | 9 +- .../src/DownloadManager.cs | 8 +- .../src/LeastRecentlyUsedDownloadProviders.cs | 1 + .../src/Providers/DownloadProviderBase.cs | 2 +- .../AlwaysValidDownloadValidator.cs | 2 +- .../src/Validation/HashDownloadValidator.cs | 2 +- .../src/Validation/IDownloadValidator.cs | 2 +- .../src/Validation/SizeDownloadValidator.cs | 2 +- ...ommonUtilities.DownloadManager.Test.csproj | 25 +- .../test/DownloadManagerTest.cs | 4 +- .../LeastRecentlyUsedDownloadProvidersTest.cs | 2 +- .../Providers/DownloadProviderTestBase.cs | 12 +- .../test/Providers/FileDownloadTest.cs | 8 +- .../test/Providers/InternetDownloadTest.cs | 4 +- .../AlwaysValidDownloadValidatorTest.cs | 2 +- .../Validation/HashDownloadValidatorTest.cs | 49 ++-- .../Validation/SizeDownloadValidatorTest.cs | 4 +- .../src/Commonutilities.FileSystem.csproj | 6 +- .../src/Extensions/FileExtensions.cs | 2 +- .../PathExtensions.NetFramework.Join.cs | 38 +-- .../Extensions/PathExtensions.NetFramework.cs | 2 +- .../Windows/WindowsFileSystemExtensions.cs | 4 +- .../src/Windows/WindowsPathExtensions.cs | 2 +- .../CommonUtilities.FileSystem.Test.csproj | 20 +- .../test/DirectoryCopierTest.cs | 22 +- .../test/DirectoryInfoExtensionsTest.cs | 22 +- .../test/FileInfoExtensionsTest.cs | 2 +- .../test/FileSystemInfoExtensionTest.cs | 2 +- .../test/PathExtensionsTest.AreEqual.cs | 4 +- .../test/PathExtensionsTest.GetFileName.cs | 2 +- .../test/PathExtensionsTest.GetPathRoot.cs | 2 +- .../PathExtensionsTest.GetRelativePathEx.cs | 2 +- ...hExtensionsTest.HasLeadingPathSeparator.cs | 2 +- ...ExtensionsTest.HasTrailingPathSeparator.cs | 2 +- .../test/PathExtensionsTest.IsChildOf.cs | 4 +- .../PathExtensionsTest.IsDriveRelativePath.cs | 4 +- ...PathExtensionsTest.IsPathFullyQualified.cs | 5 +- .../test/PathExtensionsTest.Join.cs | 4 + .../test/PathNormalizerTest.Normalize.cs | 1 + .../test/Windows/WindowsPathServiceTest.cs | 2 +- .../src/CommonUtilities.Registry.csproj | 8 +- .../test/CommonUtilities.Registry.Test.csproj | 15 +- .../test/CompilerHelpers/Attributes.cs | 45 ++++ .../RegistryKeyExtensionsTestBase.cs | 31 +-- .../Extensions/WindowsKeyExtensionsTest.cs | 2 + .../test/RegistryKey_DeleteKey_Recursive.cs | 2 +- .../test/RegistryKey_TypeLimit.cs | 9 +- src/CommonUtilities.Registry/test/TestData.cs | 8 +- .../test/WindowsRegistryTests.cs | 2 +- .../src/CommonUtilities.SimplePipeline.csproj | 10 +- ...CommonUtilities.SimplePipeline.Test.csproj | 23 +- .../ParallelProducerConsumerPipelineTest.cs | 10 +- .../test/Pipelines/PipelineTest.cs | 14 +- .../test/Pipelines/SequentialPipelineTests.cs | 4 +- .../test/Pipelines/StepRunnerPipelineTest.cs | 8 +- .../AggregatedProgressReporterTests.cs | 2 +- .../ParallelProducerConsumerStepRunnerTest.cs | 4 +- .../test/Runners/ParallelStepRunnerTest.cs | 2 +- .../test/Runners/StepRunnerTestBase.cs | 10 +- .../test/StepErrorEventArgsTest.cs | 2 +- .../test/StepFailureExceptionTests.cs | 2 +- .../test/Steps/PipelineStepTest.cs | 2 +- .../test/Steps/RunPipelineStepTest.cs | 2 +- .../test/Steps/SynchronizedStepTest.cs | 4 +- .../test/Steps/WaitStepTest.cs | 2 +- .../test/TestStep.cs | 5 +- .../PlatformSpecificFactAttribute.cs | 36 +++ .../PlatformSpecificTheoryAttribute.cs | 36 +++ .../Attributes/TestPlatformIdentifier.cs | 23 ++ .../Collections/CollectionsTestSuite.cs | 48 +++- .../Collections/ICollectionTestSuite.cs | 96 ++++++-- .../Collections/IEnumerableTestSuite.cs | 34 +++ .../Collections/IListTestSuite.cs | 35 +-- .../IReadOnlyCollectionTestSuite.cs | 8 + .../Collections/IReadOnlyListTestSuite.cs | 24 +- .../Collections/ModifyEnumerableList.cs | 20 ++ .../CommonUtilities.Testing.csproj} | 39 +-- .../ConstantHashCodeEqualityComparer.cs | 42 ++++ .../ReferenceEqualityComparer.cs | 48 ++++ .../Extensions/AssertExtensions.cs | 163 +++++++++++++ .../Extensions/RandomExtensions.cs | 223 ++++++++++++++++++ .../Extensions/StringExtensions.cs | 58 +++++ .../TestBaseWithFileSystem.cs | 63 +++++ .../TestBaseWithServiceProvider.cs | 42 ++++ src/CommonUtilities.Testing/TestingHelpers.cs | 47 ++++ .../AssertExtensions.cs | 61 ----- .../CollectionAsserts.cs | 24 -- .../CommonTestBase.cs | 26 -- .../ConditionalFactAttribute.cs | 35 --- .../EqualityComparerConstantHashCode.cs | 8 - .../src/CommonUtilities.csproj | 8 +- src/CommonUtilities/src/DisposableObject.cs | 6 +- .../src/Extensions/EncodingExtensions.cs | 2 +- .../test/AwaitExtensionsTests.cs | 14 +- .../test/Collections/DebugViewTest.cs | 4 +- .../FrugalList/FrugalListTestBase.cs | 11 +- .../FrugalList/ReadOnlyFrugalListTestBase.cs | 4 +- .../IReadOnlyValueListDictionaryTestBase.cs | 8 +- .../IValueListDictionaryTestBase.cs | 40 ++-- .../ReadOnlyValueListDictionaryTestBase.cs | 2 +- .../test/CommonUtilities.Test.csproj | 19 +- .../test/Extensions/EncodingExtensionsTest.cs | 2 + .../test/Hashing/HashingServiceTest.cs | 36 +-- src/CommonUtilities/test/ThrowHelperTest.cs | 16 +- 108 files changed, 1414 insertions(+), 538 deletions(-) create mode 100644 global.json create mode 100644 src/CommonUtilities.Registry/test/CompilerHelpers/Attributes.cs create mode 100644 src/CommonUtilities.Testing/Attributes/PlatformSpecificFactAttribute.cs create mode 100644 src/CommonUtilities.Testing/Attributes/PlatformSpecificTheoryAttribute.cs create mode 100644 src/CommonUtilities.Testing/Attributes/TestPlatformIdentifier.cs rename src/{CommonUtilities.TestingUtilities => CommonUtilities.Testing}/Collections/CollectionsTestSuite.cs (75%) rename src/{CommonUtilities.TestingUtilities => CommonUtilities.Testing}/Collections/ICollectionTestSuite.cs (85%) rename src/{CommonUtilities.TestingUtilities => CommonUtilities.Testing}/Collections/IEnumerableTestSuite.cs (95%) rename src/{CommonUtilities.TestingUtilities => CommonUtilities.Testing}/Collections/IListTestSuite.cs (96%) rename src/{CommonUtilities.TestingUtilities => CommonUtilities.Testing}/Collections/IReadOnlyCollectionTestSuite.cs (88%) rename src/{CommonUtilities.TestingUtilities => CommonUtilities.Testing}/Collections/IReadOnlyListTestSuite.cs (80%) create mode 100644 src/CommonUtilities.Testing/Collections/ModifyEnumerableList.cs rename src/{CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj => CommonUtilities.Testing/CommonUtilities.Testing.csproj} (54%) create mode 100644 src/CommonUtilities.Testing/EqualityComparers/ConstantHashCodeEqualityComparer.cs create mode 100644 src/CommonUtilities.Testing/EqualityComparers/ReferenceEqualityComparer.cs create mode 100644 src/CommonUtilities.Testing/Extensions/AssertExtensions.cs create mode 100644 src/CommonUtilities.Testing/Extensions/RandomExtensions.cs create mode 100644 src/CommonUtilities.Testing/Extensions/StringExtensions.cs create mode 100644 src/CommonUtilities.Testing/TestBaseWithFileSystem.cs create mode 100644 src/CommonUtilities.Testing/TestBaseWithServiceProvider.cs create mode 100644 src/CommonUtilities.Testing/TestingHelpers.cs delete mode 100644 src/CommonUtilities.TestingUtilities/AssertExtensions.cs delete mode 100644 src/CommonUtilities.TestingUtilities/CollectionAsserts.cs delete mode 100644 src/CommonUtilities.TestingUtilities/CommonTestBase.cs delete mode 100644 src/CommonUtilities.TestingUtilities/ConditionalFactAttribute.cs delete mode 100644 src/CommonUtilities.TestingUtilities/EqualityComparerConstantHashCode.cs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 233e4a6..c73c4fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,4 +29,4 @@ jobs: 10.0.x - name: Build & Test in Release Mode - run: dotnet test --configuration Release --logger "GitHubActions" \ No newline at end of file + run: dotnet test --configuration Release --report-github \ No newline at end of file diff --git a/CommonUtilities.slnx b/CommonUtilities.slnx index da8c736..09d7c97 100644 --- a/CommonUtilities.slnx +++ b/CommonUtilities.slnx @@ -19,5 +19,5 @@ - + diff --git a/Directory.Build.props b/Directory.Build.props index 9fa329e..f310960 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,15 +1,19 @@ $(MSBuildThisFileDirectory) - preview - disable true true $(MSBuildThisFileDirectory) $(RepoRootPath)bin\Packages\$(Configuration)\ - .NET Common Utilities + preview + disable + enable + true + + + AnakinRaW Common Utilities Copyright © AnakinRaW 2025 AnakinRaW AnakinRaW diff --git a/global.json b/global.json new file mode 100644 index 0000000..8f73781 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index 7f6efce..e80e4fc 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -1,16 +1,17 @@  - Simple Download Manager supporting the local file system and HTTP downlaods. + CommonUtilities.DownloadManager + AnakinRaW.CommonUtilities.DownloadManager + Simple DownloadAsync Manager supporting the local file system and HTTP downlaods. + true netstandard2.0;netstandard2.1;net10.0 AnakinRaW.CommonUtilities.DownloadManager AnakinRaW.CommonUtilities.DownloadManager - enable - True - true + en diff --git a/src/CommonUtilities.DownloadManager/src/DownloadManager.cs b/src/CommonUtilities.DownloadManager/src/DownloadManager.cs index 55f7b15..aea576f 100644 --- a/src/CommonUtilities.DownloadManager/src/DownloadManager.cs +++ b/src/CommonUtilities.DownloadManager/src/DownloadManager.cs @@ -90,7 +90,7 @@ public Task DownloadAsync( if (!uri.IsAbsoluteUri) throw new ArgumentException("Uri must be absolute.", nameof(uri)); - _logger?.LogTrace("Download requested: {Uri}", uri.AbsoluteUri); + _logger?.LogTrace("DownloadAsync requested: {Uri}", uri.AbsoluteUri); if (uri is { IsFile: false, IsUnc: false }) { @@ -172,7 +172,7 @@ private async Task DownloadWithRetry( bool validationSuccess; try { - validationSuccess = await validator.Validate(outputStream, summary.DownloadedSize, cancellationToken) + validationSuccess = await validator.ValidateAsync(outputStream, summary.DownloadedSize, cancellationToken) .ConfigureAwait(false); } catch (Exception e) @@ -193,7 +193,7 @@ private async Task DownloadWithRetry( } } - _logger?.LogInformation("Download of '{Uri}' succeeded using provider '{ProviderName}'", uri.AbsoluteUri, provider.Name); + _logger?.LogInformation("DownloadAsync of '{Uri}' succeeded using provider '{ProviderName}'", uri.AbsoluteUri, provider.Name); _leastRecentlyUsedDownloadProviders.LastSuccessfulProvider = provider.Name; summary.DownloadProvider = provider.Name; @@ -207,7 +207,7 @@ private async Task DownloadWithRetry( catch (Exception ex) { failureList.Add(new DownloadFailureInformation(ex, provider.Name)); - _logger?.LogTrace("Download failed using {Provider} provider. {Exception}", provider.Name, ex); + _logger?.LogTrace("DownloadAsync failed using {Provider} provider. {Exception}", provider.Name, ex); if (provider.Equals(providers.LastOrDefault())) throw new DownloadFailedException(failureList); diff --git a/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs b/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs index 9af186b..16d5a4c 100644 --- a/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs +++ b/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs @@ -27,6 +27,7 @@ public string? LastSuccessfulProvider if (value == null) return; field = value; + // ReSharper disable once InconsistentlySynchronizedField _preferredProviders.AddOrUpdate(value, 1, (_, existingVal) => ++existingVal); } } diff --git a/src/CommonUtilities.DownloadManager/src/Providers/DownloadProviderBase.cs b/src/CommonUtilities.DownloadManager/src/Providers/DownloadProviderBase.cs index c3800df..e85fcdc 100644 --- a/src/CommonUtilities.DownloadManager/src/Providers/DownloadProviderBase.cs +++ b/src/CommonUtilities.DownloadManager/src/Providers/DownloadProviderBase.cs @@ -68,7 +68,7 @@ public Task DownloadAsync( /// /// Concrete implementation for downloading a file. /// - /// Download time and bit rate is automatically set after this method returns. + /// DownloadAsync time and bit rate is automatically set after this method returns. /// The location of the source file. /// The output stream. /// Progress with already updated performance data. diff --git a/src/CommonUtilities.DownloadManager/src/Validation/AlwaysValidDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/AlwaysValidDownloadValidator.cs index 3eac4b9..3bcdf86 100644 --- a/src/CommonUtilities.DownloadManager/src/Validation/AlwaysValidDownloadValidator.cs +++ b/src/CommonUtilities.DownloadManager/src/Validation/AlwaysValidDownloadValidator.cs @@ -19,7 +19,7 @@ private AlwaysValidDownloadValidator() } /// - public Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default) + public Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default) { return Task.FromResult(true); } diff --git a/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs index 636952e..f29d196 100644 --- a/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs +++ b/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs @@ -37,7 +37,7 @@ public HashDownloadValidator(byte[]? hash, HashTypeKey hashType, IServiceProvide } /// - public async Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default) + public async Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default) { if (stream == null) throw new ArgumentNullException(nameof(stream)); diff --git a/src/CommonUtilities.DownloadManager/src/Validation/IDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/IDownloadValidator.cs index 9d8f626..24c231f 100644 --- a/src/CommonUtilities.DownloadManager/src/Validation/IDownloadValidator.cs +++ b/src/CommonUtilities.DownloadManager/src/Validation/IDownloadValidator.cs @@ -19,5 +19,5 @@ public interface IDownloadValidator /// The number of bytes downloaded. /// The cancellation token. /// if the download is valid; otherwise, . - Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default); + Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default); } \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/src/Validation/SizeDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/SizeDownloadValidator.cs index ace6745..4b22c4e 100644 --- a/src/CommonUtilities.DownloadManager/src/Validation/SizeDownloadValidator.cs +++ b/src/CommonUtilities.DownloadManager/src/Validation/SizeDownloadValidator.cs @@ -21,7 +21,7 @@ public SizeDownloadValidator(long expectedDownloadBytes) } /// - public Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default) + public Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default) { return Task.FromResult(_expectedDownloadBytes == downloadedBytes); } diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 3861f54..3652bc0 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -1,10 +1,11 @@ - + net10.0;net8.0 $(TargetFrameworks);net481 false true + Exe @@ -12,21 +13,17 @@ AnakinRaW.CommonUtilities.DownloadManager.Test - - enable - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -38,8 +35,8 @@ - + \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs index 58cb97b..e02a022 100644 --- a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs +++ b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs @@ -15,7 +15,7 @@ namespace AnakinRaW.CommonUtilities.DownloadManager.Test; -public class DownloadManagerTest : CommonTestBase +public class DownloadManagerTest : TestBaseWithFileSystem { private const string Destination = "file.txt"; @@ -464,7 +464,7 @@ void ProgressMethod(DownloadUpdate status) private class ThrowingValidator : IDownloadValidator { - public Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default) + public Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default) { throw new Exception("Test"); } diff --git a/src/CommonUtilities.DownloadManager/test/LeastRecentlyUsedDownloadProvidersTest.cs b/src/CommonUtilities.DownloadManager/test/LeastRecentlyUsedDownloadProvidersTest.cs index d0f7a09..f8ed92d 100644 --- a/src/CommonUtilities.DownloadManager/test/LeastRecentlyUsedDownloadProvidersTest.cs +++ b/src/CommonUtilities.DownloadManager/test/LeastRecentlyUsedDownloadProvidersTest.cs @@ -6,7 +6,7 @@ namespace AnakinRaW.CommonUtilities.DownloadManager.Test; -public class LeastRecentlyUsedDownloadProvidersTest : CommonTestBase +public class LeastRecentlyUsedDownloadProvidersTest : TestBaseWithFileSystem { private readonly LeastRecentlyUsedDownloadProviders _provider = new(); diff --git a/src/CommonUtilities.DownloadManager/test/Providers/DownloadProviderTestBase.cs b/src/CommonUtilities.DownloadManager/test/Providers/DownloadProviderTestBase.cs index 1458a7d..a155f99 100644 --- a/src/CommonUtilities.DownloadManager/test/Providers/DownloadProviderTestBase.cs +++ b/src/CommonUtilities.DownloadManager/test/Providers/DownloadProviderTestBase.cs @@ -8,7 +8,7 @@ namespace AnakinRaW.CommonUtilities.DownloadManager.Test.Providers; -public abstract class DownloadProviderTestBase : CommonTestBase +public abstract class DownloadProviderTestBase : TestBaseWithFileSystem { protected abstract Type ExpectedSourceNotFoundExceptionType { get; } @@ -20,7 +20,7 @@ public abstract class DownloadProviderTestBase : CommonTestBase public async Task DownloadAsync_SourceNotFound_Throws() { var source = CreateSource(false); - await Assert.ThrowsAsync(ExpectedSourceNotFoundExceptionType, async () => await Download(source, new MemoryStream(), null)); + await Assert.ThrowsAsync(ExpectedSourceNotFoundExceptionType, async () => await TestDownloadAsync(source, new MemoryStream(), null, TestContext.Current.CancellationToken)); } [Theory] @@ -32,7 +32,7 @@ public async Task DownloadAsync(bool createDefaultOptions) var source = CreateSource(true); var options = createDefaultOptions ? new DownloadOptions() : null; - var result = await Download(source, outStream, options); + var result = await TestDownloadAsync(source, outStream, options, TestContext.Current.CancellationToken); Assert.True(result.DownloadedSize > 0); Assert.Equal(result.DownloadedSize, outStream.Length); } @@ -45,10 +45,10 @@ public async Task DownloadAsync_DownloadCancelled_Throws(bool createDefaultOptio var outStream = new MemoryStream(); var source = CreateSource(true); var options = createDefaultOptions ? new DownloadOptions() : null; - await Assert.ThrowsAnyAsync(async () => await Download(source, outStream, options, new CancellationToken(true))); + await Assert.ThrowsAnyAsync(async () => await TestDownloadAsync(source, outStream, options, new CancellationToken(true))); } - protected async Task Download(Uri source, Stream outStream, DownloadOptions? options, CancellationToken token = default) + protected async Task TestDownloadAsync(Uri source, Stream outStream, DownloadOptions? options, CancellationToken token = default) { var provider = CreateProvider(); @@ -65,7 +65,7 @@ protected async Task Download(Uri source, Stream outStream, Down void Callback(DownloadUpdate status) { - Task.Delay(100).Wait(); + Task.Delay(100, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); callBackFired = true; } } diff --git a/src/CommonUtilities.DownloadManager/test/Providers/FileDownloadTest.cs b/src/CommonUtilities.DownloadManager/test/Providers/FileDownloadTest.cs index 16b71c6..6b1f4a7 100644 --- a/src/CommonUtilities.DownloadManager/test/Providers/FileDownloadTest.cs +++ b/src/CommonUtilities.DownloadManager/test/Providers/FileDownloadTest.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; using AnakinRaW.CommonUtilities.DownloadManager.Providers; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Xunit; namespace AnakinRaW.CommonUtilities.DownloadManager.Test.Providers; @@ -41,7 +41,7 @@ public async Task DownloadAsync_ExpectedData() var source = CreateSource(true); var outStream = new MemoryStream(); - var result = await Download(source, outStream, null, CancellationToken.None); + var result = await TestDownloadAsync(source, outStream, null, CancellationToken.None); Assert.Equal(Data.Length, result.DownloadedSize); var copyData = Encoding.Default.GetString(outStream.ToArray()); @@ -53,7 +53,7 @@ public async Task DownloadAsync_UncPath() { var source = new Uri("file://server/test.file"); Assert.True(source.IsUnc); - await Assert.ThrowsAsync(ExpectedSourceNotFoundExceptionType, async () => await Download(source, new MemoryStream(), null)); + await Assert.ThrowsAsync(ExpectedSourceNotFoundExceptionType, async () => await TestDownloadAsync(source, new MemoryStream(), null)); } [Theory] @@ -65,6 +65,6 @@ public async Task DownloadAsync_NotAFileSource_Throws(string uri) { var source = new Uri(uri); var outStream = new MemoryStream(); - await Assert.ThrowsAsync(async () => await Download(source, outStream, null, CancellationToken.None)); + await Assert.ThrowsAsync(async () => await TestDownloadAsync(source, outStream, null, CancellationToken.None)); } } \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/test/Providers/InternetDownloadTest.cs b/src/CommonUtilities.DownloadManager/test/Providers/InternetDownloadTest.cs index 6dbf15a..6cb5d53 100644 --- a/src/CommonUtilities.DownloadManager/test/Providers/InternetDownloadTest.cs +++ b/src/CommonUtilities.DownloadManager/test/Providers/InternetDownloadTest.cs @@ -28,7 +28,7 @@ public async Task DownloadAsync_WithUserAgent() UserAgent = "AnakinRaw.DownloadManager.Test" }; - var result = await Download(source, outStream, options); + var result = await TestDownloadAsync(source, outStream, options, TestContext.Current.CancellationToken); Assert.True(result.DownloadedSize > 0); Assert.Equal(result.DownloadedSize, outStream.Length); } @@ -39,7 +39,7 @@ public async Task DownloadAsync_RequiredUserAgentNotSet_Throws() var outStream = new MemoryStream(); var source = new Uri("https://api.github.com/repos/AnakinRaW/CommonUtilities/releases/latest"); - var exception = await Assert.ThrowsAnyAsync(async () => await Download(source, outStream, null)); + var exception = await Assert.ThrowsAnyAsync(async () => await TestDownloadAsync(source, outStream, null, TestContext.Current.CancellationToken)); AssertRequiredUserAgentMissingException(exception); } } \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/test/Validation/AlwaysValidDownloadValidatorTest.cs b/src/CommonUtilities.DownloadManager/test/Validation/AlwaysValidDownloadValidatorTest.cs index dd5b10e..72bcc79 100644 --- a/src/CommonUtilities.DownloadManager/test/Validation/AlwaysValidDownloadValidatorTest.cs +++ b/src/CommonUtilities.DownloadManager/test/Validation/AlwaysValidDownloadValidatorTest.cs @@ -11,7 +11,7 @@ public class AlwaysValidDownloadValidatorTest public async Task Validate_IsValid_NullStream_CancelledToken_NegativeBytes() { var validator = AlwaysValidDownloadValidator.Instance; - var result = await validator.Validate(null!, -1, new CancellationToken(true)); + var result = await validator.ValidateAsync(null!, -1, new CancellationToken(true)); Assert.True(result); } } \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs b/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs index 1b83269..621674b 100644 --- a/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs +++ b/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs @@ -13,7 +13,7 @@ namespace AnakinRaW.CommonUtilities.DownloadManager.Test.Validation; -public class HashDownloadValidatorTest : CommonTestBase +public class HashDownloadValidatorTest : TestBaseWithFileSystem { protected override void SetupServices(IServiceCollection serviceCollection) { @@ -76,14 +76,14 @@ public void Ctor_InvalidateCorrect(HashTypeKey type, byte[] data) public async Task Validate_NullStream_Throws() { var validator = new HashDownloadValidator(null, HashTypeKey.None, ServiceProvider); - await Assert.ThrowsAsync(async () => await validator.Validate(null!, 0)); + await Assert.ThrowsAsync(async () => await validator.ValidateAsync(null!, 0, TestContext.Current.CancellationToken)); } [Fact] public async Task Validate_NoneHashType() { var validator = new HashDownloadValidator(null, HashTypeKey.None, ServiceProvider); - var result = await validator.Validate(new MemoryStream(new byte[3]), 0); + var result = await validator.ValidateAsync(new MemoryStream(new byte[3]), 0, TestContext.Current.CancellationToken); Assert.True(result); } @@ -92,7 +92,7 @@ public async Task Validate_StreamNotSeekable_ThrowsNotSupportedException() { var validator = new HashDownloadValidator(null, HashTypeKey.None, ServiceProvider); await Assert.ThrowsAsync(async () => - await validator.Validate(new NonSeekableStream(), 0)); + await validator.ValidateAsync(new NonSeekableStream(), 0, TestContext.Current.CancellationToken)); } [Theory] @@ -110,7 +110,7 @@ public async Task Validate_HashesToNotMatch(HashTypeKey hashType, byte[] notExpe // notExpectedHash is always empty var validator = new HashDownloadValidator(notExpectedHash, hashType, ServiceProvider); - var result = await validator.Validate(dlStream, actualDownloadedBytes); + var result = await validator.ValidateAsync(dlStream, actualDownloadedBytes, TestContext.Current.CancellationToken); Assert.False(result); } @@ -138,7 +138,7 @@ public async Task Validate_HashesMatch(HashTypeKey hashType, string expectedHash var expectedHash = ConvertHexStringToByteArray(expectedHashString); var validator = new HashDownloadValidator(expectedHash, hashType, ServiceProvider); - var result = await validator.Validate(dlStream, actualDownloadedBytes); + var result = await validator.ValidateAsync(dlStream, actualDownloadedBytes, TestContext.Current.CancellationToken); Assert.True(result); if (hashType != HashTypeKey.None) @@ -163,37 +163,22 @@ public static byte[] ConvertHexStringToByteArray(string hexString) } - class NonSeekableStream : Stream + private class NonSeekableStream : Stream { - public override void Flush() - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - public override bool CanRead => false; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => 0; public override long Position { get; set; } + + public override void Flush() => throw new NotImplementedException(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + + public override void SetLength(long value) => throw new NotImplementedException(); + + public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/test/Validation/SizeDownloadValidatorTest.cs b/src/CommonUtilities.DownloadManager/test/Validation/SizeDownloadValidatorTest.cs index af22cff..d50f8ac 100644 --- a/src/CommonUtilities.DownloadManager/test/Validation/SizeDownloadValidatorTest.cs +++ b/src/CommonUtilities.DownloadManager/test/Validation/SizeDownloadValidatorTest.cs @@ -10,7 +10,7 @@ public class SizeDownloadValidatorTest public async Task Validate_IsValid() { var validator = new SizeDownloadValidator(123); - var result = await validator.Validate(null!, 123); + var result = await validator.ValidateAsync(null!, 123, TestContext.Current.CancellationToken); Assert.True(result); } @@ -20,7 +20,7 @@ public async Task Validate_IsValid() public async Task Validate_IsInvalid(int actualValue) { var validator = new SizeDownloadValidator(123); - var result = await validator.Validate(null!, actualValue); + var result = await validator.ValidateAsync(null!, actualValue, TestContext.Current.CancellationToken); Assert.False(result); } } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj index 53b6baf..7fd9e83 100644 --- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj +++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj @@ -1,15 +1,17 @@  + CommonUtilities.FileSystem + AnakinRaW.CommonUtilities.FileSystem Helper classes and methods targeting the file system. + true netstandard2.0;netstandard2.1;net10.0 AnakinRaW.CommonUtilities.FileSystem AnakinRaW.CommonUtilities.FileSystem - enable - True + en diff --git a/src/CommonUtilities.FileSystem/src/Extensions/FileExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/FileExtensions.cs index 6fdcfb5..18d69ba 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/FileExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/FileExtensions.cs @@ -70,7 +70,7 @@ public static bool MoveToEx(this IFileInfo source, string destination, bool over if (destination == null) throw new ArgumentNullException(nameof(destination)); - return MoveEx(source.FileSystem.File, source.FullName, destination, overwrite); + return source.FileSystem.File.MoveEx(source.FullName, destination, overwrite); } /// diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.Join.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.Join.cs index cefac2d..a056de7 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.Join.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.Join.cs @@ -110,11 +110,11 @@ public static string Join(this IPath _, ReadOnlySpan path1, ReadOnlySpan path1, ReadOnlySpan path2, ReadOnlySpan path3) { if (path1.Length == 0) - return Join(_, path2, path3); + return _.Join(path2, path3); if (path2.Length == 0) - return Join(_, path1, path3); + return _.Join(path1, path3); if (path3.Length == 0) - return Join(_, path1, path2); + return _.Join(path1, path2); return JoinInternal(path1, path2, path3); } @@ -187,13 +187,13 @@ public static string Join(this IPath _, ReadOnlySpan path1, ReadOnlySpan path1, ReadOnlySpan path2, ReadOnlySpan path3, ReadOnlySpan path4) { if (path1.Length == 0) - return Join(_, path2, path3, path4); + return _.Join(path2, path3, path4); if (path2.Length == 0) - return Join(_, path1, path3, path4); + return _.Join(path1, path3, path4); if (path3.Length == 0) - return Join(_, path1, path2, path4); + return _.Join(path1, path2, path4); if (path4.Length == 0) - return Join(_, path1, path2, path3); + return _.Join(path1, path2, path3); return JoinInternal(path1, path2, path3, path4); } @@ -266,7 +266,7 @@ public static string Join(this IPath _, ReadOnlySpan path1, ReadOnlySpan)paths); + return _.Join((ReadOnlySpan)paths); } /// @@ -342,11 +342,11 @@ public static bool TryJoin( return true; if (path1.Length == 0) - return TryJoin(_, path2, path3, destination, out charsWritten); + return _.TryJoin(path2, path3, destination, out charsWritten); if (path2.Length == 0) - return TryJoin(_, path1, path3, destination, out charsWritten); + return _.TryJoin(path1, path3, destination, out charsWritten); if (path3.Length == 0) - return TryJoin(_, path1, path2, destination, out charsWritten); + return _.TryJoin(path1, path2, destination, out charsWritten); var neededSeparators = HasTrailingDirectorySeparator(path1) || HasLeadingDirectorySeparator(path2) ? 0 : 1; var needsSecondSeparator = !(HasTrailingDirectorySeparator(path2) || HasLeadingDirectorySeparator(path3)); @@ -357,7 +357,7 @@ public static bool TryJoin( if (destination.Length < charsNeeded) return false; - var result = TryJoin(_, path1, path2, destination, out charsWritten); + var result = _.TryJoin(path1, path2, destination, out charsWritten); Debug.Assert(result, "should never fail joining first two paths"); if (needsSecondSeparator) diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs index 28bac17..e671209 100644 --- a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs +++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs @@ -184,7 +184,7 @@ public static bool IsPathFullyQualified(this IPath _, string path) { if (path == null) throw new ArgumentNullException(nameof(path)); - return IsPathFullyQualified(_, path.AsSpan()); + return _.IsPathFullyQualified(path.AsSpan()); } /// diff --git a/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs b/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs index 15a3447..3c5e3ce 100644 --- a/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs @@ -135,7 +135,7 @@ public static bool DeleteWithRetry(this IFileInfo file, out bool rebootRequired, rebootRequired = false; return success; } - rebootRequired = DeleteAfterReboot(file); + rebootRequired = file.DeleteAfterReboot(); return false; } @@ -173,7 +173,7 @@ public static bool DeleteWithRetry(this IDirectoryInfo directory, out bool reboo return success; } - rebootRequired = DeleteAfterReboot(directory); + rebootRequired = directory.DeleteAfterReboot(); return false; } } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs b/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs index 2cd00d8..040317e 100644 --- a/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs +++ b/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs @@ -38,7 +38,7 @@ public static bool UserHasDirectoryAccessRights(this IDirectoryInfo directoryInf { if (!directoryInfo.Exists) throw new DirectoryNotFoundException($"Unable to find {directoryInfo.FullName}"); - isInRoleWithAccess = TestAccessRightsOnWindows(directoryInfo, accessRights); + isInRoleWithAccess = directoryInfo.TestAccessRightsOnWindows(accessRights); } catch (UnauthorizedAccessException) { diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index f98eaed..32064da 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -1,11 +1,11 @@ - + net10.0;net8.0 $(TargetFrameworks);net481 false true - enable + Exe @@ -15,21 +15,21 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - all runtime; build; native; contentfiles; analyzers; buildtransitive - - runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -41,8 +41,8 @@ - + diff --git a/src/CommonUtilities.FileSystem/test/DirectoryCopierTest.cs b/src/CommonUtilities.FileSystem/test/DirectoryCopierTest.cs index 4249085..d47770e 100644 --- a/src/CommonUtilities.FileSystem/test/DirectoryCopierTest.cs +++ b/src/CommonUtilities.FileSystem/test/DirectoryCopierTest.cs @@ -30,15 +30,15 @@ public void MoveDirectory_CopyDirectory_ThrowsArgumentNullExceptions() public async Task MoveDirectoryAsync_CopyDirectoryAsync_ThrowsArgumentNullExceptionsAsync() { var copier = new DirectoryCopier(_fileSystem); - await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync(null!, "path")); - await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", null!)); - await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", "path", null, null, 0)); - await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", "path", null, null, -1)); - - await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync(null!, "path")); - await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", null!)); - await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", "path", null, null, 0)); - await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", "path", null, null, -1)); + await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync(null!, "path", cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", null!, cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", "path", null, null, 0, cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", "path", null, null, -1, cancellationToken: TestContext.Current.CancellationToken)); + + await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync(null!, "path", cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", null!, cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", "path", null, null, 0, cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", "path", null, null, -1, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -101,7 +101,7 @@ public async Task CopyDirectoryAsync() // https://github.com/Testably/Testably.Abstractions/issues/549 var fsStream = _fileSystem.FileStream.New("test/1.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await copier.CopyDirectoryAsync("test", "other", progress, Exclude2); + await copier.CopyDirectoryAsync("test", "other", progress, Exclude2, cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(1.0, progressValue); Assert.True(_fileSystem.File.Exists("other/1.txt")); @@ -207,7 +207,7 @@ public async Task MoveDirectoryAsync() }); - var delSuc = await copier.MoveDirectoryAsync("test", "other", progress); + var delSuc = await copier.MoveDirectoryAsync("test", "other", progress, cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(1.0, progressValue); Assert.True(delSuc); Assert.False(_fileSystem.Directory.Exists("test")); diff --git a/src/CommonUtilities.FileSystem/test/DirectoryInfoExtensionsTest.cs b/src/CommonUtilities.FileSystem/test/DirectoryInfoExtensionsTest.cs index dc93941..17338f7 100644 --- a/src/CommonUtilities.FileSystem/test/DirectoryInfoExtensionsTest.cs +++ b/src/CommonUtilities.FileSystem/test/DirectoryInfoExtensionsTest.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions.Testing; using Xunit; @@ -199,7 +199,7 @@ public async Task MoveToAsync_ThrowsDirectoryNotFound() _fileSystem.Initialize(); var dirToMove = _fileSystem.DirectoryInfo.New("test"); await Assert.ThrowsAsync(async () => - await dirToMove.MoveToAsync("test1", null, DirectoryOverwriteOption.NoOverwrite)); + await dirToMove.MoveToAsync("test1", null, DirectoryOverwriteOption.NoOverwrite, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -211,7 +211,7 @@ public async Task MoveToAsync_NoOverwrite_ThrowsIOException() .WithSubdirectory("other"); var dirToMove = _fileSystem.DirectoryInfo.New("test"); - await Assert.ThrowsAsync(async () => await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.NoOverwrite)); + await Assert.ThrowsAsync(async () => await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.NoOverwrite, cancellationToken: TestContext.Current.CancellationToken)); } @@ -228,7 +228,7 @@ public async Task MoveToAsync_CleanOverride() var dirToMove = _fileSystem.DirectoryInfo.New("test"); - var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.CleanOverwrite); + var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.CleanOverwrite, cancellationToken: TestContext.Current.CancellationToken); Assert.True(delSuc); Assert.False(_fileSystem.Directory.Exists("test")); Assert.True(_fileSystem.Directory.Exists("other")); @@ -252,7 +252,7 @@ public async Task MoveToAsync_MergeOverride() var dirToMove = _fileSystem.DirectoryInfo.New("test"); - var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.MergeOverwrite); + var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.MergeOverwrite, cancellationToken: TestContext.Current.CancellationToken); Assert.True(delSuc); Assert.False(_fileSystem.Directory.Exists("test")); Assert.True(_fileSystem.Directory.Exists("other")); @@ -289,7 +289,7 @@ public async Task MoveToAsync_WithProgress() { progressValue = d; }); - var delSuc = await dirToMove.MoveToAsync("other", progress, DirectoryOverwriteOption.CleanOverwrite); + var delSuc = await dirToMove.MoveToAsync("other", progress, DirectoryOverwriteOption.CleanOverwrite, cancellationToken: TestContext.Current.CancellationToken); Assert.True(delSuc); Assert.Equal(1.0, progressValue); @@ -307,7 +307,7 @@ public async Task MoveToAsync_CannotDeleteSource() var fs = _fileSystem.FileStream.New("test/1.txt", FileMode.Open, FileAccess.Read, FileShare.Read); - var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.CleanOverwrite); + var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.CleanOverwrite, cancellationToken: TestContext.Current.CancellationToken); Assert.False(delSuc); fs.Dispose(); @@ -395,7 +395,7 @@ public void Copy_AcrossDrives() public async Task CopyAsync_ThrowsDirectoryNotFound() { var dirToCopy = _fileSystem.DirectoryInfo.New("test"); - await Assert.ThrowsAsync(async () => await dirToCopy.CopyAsync("test1", null, DirectoryOverwriteOption.NoOverwrite)); + await Assert.ThrowsAsync(async () => await dirToCopy.CopyAsync("test1", null, DirectoryOverwriteOption.NoOverwrite, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -408,7 +408,7 @@ public async Task CopyAsync_NoOverwrite_ThrowsIOException() var dirToCopy = _fileSystem.DirectoryInfo.New("test"); - await Assert.ThrowsAsync(async () => await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.NoOverwrite)); + await Assert.ThrowsAsync(async () => await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.NoOverwrite, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -423,7 +423,7 @@ public async Task CopyAsync_CleanOverwrite() var dirToCopy = _fileSystem.DirectoryInfo.New("test"); - await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.CleanOverwrite); + await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.CleanOverwrite, cancellationToken: TestContext.Current.CancellationToken); Assert.True(_fileSystem.Directory.Exists("test")); Assert.True(_fileSystem.Directory.Exists("other")); Assert.Equal(3, _fileSystem.DirectoryInfo.New("other").GetFiles("*", SearchOption.AllDirectories).Length); @@ -445,7 +445,7 @@ public async Task CopyAsync_MergeOverwrite() var dirToCopy = _fileSystem.DirectoryInfo.New("test"); - await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.MergeOverwrite); + await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.MergeOverwrite, cancellationToken: TestContext.Current.CancellationToken); Assert.True(_fileSystem.Directory.Exists("test")); Assert.True(_fileSystem.Directory.Exists("other")); Assert.Equal(5, _fileSystem.DirectoryInfo.New("other").GetFiles("*", SearchOption.AllDirectories).Length); diff --git a/src/CommonUtilities.FileSystem/test/FileInfoExtensionsTest.cs b/src/CommonUtilities.FileSystem/test/FileInfoExtensionsTest.cs index 2c4682b..c3a8fb9 100644 --- a/src/CommonUtilities.FileSystem/test/FileInfoExtensionsTest.cs +++ b/src/CommonUtilities.FileSystem/test/FileInfoExtensionsTest.cs @@ -1,6 +1,6 @@ using System.IO; using System.Runtime.InteropServices; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions.Testing; using Xunit; diff --git a/src/CommonUtilities.FileSystem/test/FileSystemInfoExtensionTest.cs b/src/CommonUtilities.FileSystem/test/FileSystemInfoExtensionTest.cs index 5c0f8c5..d1ccfdd 100644 --- a/src/CommonUtilities.FileSystem/test/FileSystemInfoExtensionTest.cs +++ b/src/CommonUtilities.FileSystem/test/FileSystemInfoExtensionTest.cs @@ -1,4 +1,4 @@ -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions.Testing; using Xunit; diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.AreEqual.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.AreEqual.cs index 2b040fa..1b45404 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.AreEqual.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.AreEqual.cs @@ -1,6 +1,6 @@ -using AnakinRaW.CommonUtilities.Testing; -using System; +using System; using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Xunit; diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs index 0b3f321..92f3ef9 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Abstractions; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Xunit; // ReSharper disable InconsistentNaming diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs index 464f5d8..5b13789 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.IO.Abstractions; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Xunit; // ReSharper disable InconsistentNaming diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs index 3beb813..a49b3e1 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs @@ -1,6 +1,6 @@ using System; using System.IO.Abstractions; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Testably.Abstractions.Testing; using Xunit; diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs index e33c622..61d863a 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs @@ -1,6 +1,6 @@ using System; using System.IO.Abstractions; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Xunit; // ReSharper disable InconsistentNaming diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs index 3422769..831ab59 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs @@ -1,6 +1,6 @@ using System; using System.IO.Abstractions; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Xunit; #if NET diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsChildOf.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsChildOf.cs index 360083d..5e34019 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsChildOf.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsChildOf.cs @@ -1,6 +1,6 @@ -using AnakinRaW.CommonUtilities.Testing; -using System; +using System; using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions.Testing; using Xunit; diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsDriveRelativePath.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsDriveRelativePath.cs index c5951ee..68d1a8b 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsDriveRelativePath.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsDriveRelativePath.cs @@ -1,6 +1,6 @@ -using AnakinRaW.CommonUtilities.Testing; -using System; +using System; using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Xunit; diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs index 71886a3..d7426f2 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs @@ -1,6 +1,6 @@ -using AnakinRaW.CommonUtilities.Testing; -using System; +using System; using System.IO.Abstractions; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Xunit; @@ -13,6 +13,7 @@ public class IsPathFullyQualifiedTest [Fact] public void IsPathFullyQualified_NullArgument() { + // ReSharper disable once RedundantCast Assert.Throws(() => _fileSystem.Path.IsPathFullyQualified(((string?)null)!)); } diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs index 9cafe96..2fc37d2 100644 --- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs +++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs @@ -190,6 +190,8 @@ public void TryJoinThreePaths(string? path1, string? path2, string? path3, strin { "a", null, null, "b", $"a{Sep}b" } }; + // ReSharper disable RedundantExplicitParamsArrayCreation + [Theory, MemberData(nameof(TestData_JoinFourPaths))] public void JoinFourPaths(string? path1, string? path2, string? path3, string? path4, string expected) { @@ -247,4 +249,6 @@ public void JoinStringArray_8(string? path1, string? path2, string? path3, strin Assert.Equal(_fileSystem.Path.Join(fourJoined, fourJoined), _fileSystem.Path.Join((ReadOnlySpan)new[] { path1, path2, path3, path4, path1, path2, path3, path4 })); } + + // ReSharper restore RedundantExplicitParamsArrayCreation } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs b/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs index dbef7e4..8e5db2b 100644 --- a/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs +++ b/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs @@ -13,6 +13,7 @@ public void Normalize_Throws() { Assert.Throws(() => { + // ReSharper disable once RedundantCast PathNormalizer.Normalize((string)null!, new PathNormalizeOptions()); }); Assert.Throws(() => diff --git a/src/CommonUtilities.FileSystem/test/Windows/WindowsPathServiceTest.cs b/src/CommonUtilities.FileSystem/test/Windows/WindowsPathServiceTest.cs index 0326e74..5c97c38 100644 --- a/src/CommonUtilities.FileSystem/test/Windows/WindowsPathServiceTest.cs +++ b/src/CommonUtilities.FileSystem/test/Windows/WindowsPathServiceTest.cs @@ -1,7 +1,7 @@ using System.IO; using System.Security.AccessControl; using AnakinRaW.CommonUtilities.FileSystem.Windows; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Testably.Abstractions; using Xunit; #if NET diff --git a/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj b/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj index a2ef07f..7a6efaa 100644 --- a/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj +++ b/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj @@ -1,15 +1,17 @@ - + + CommonUtilities.Registry + AnakinRaW.CommonUtilities.Registry Platform independent abstraction layer for the Windows Registry. + true netstandard2.0;net10.0 AnakinRaW.CommonUtilities.Registry AnakinRaW.CommonUtilities.Registry - enable - True + en diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj index cb77051..a9c4a3d 100644 --- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj +++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj @@ -5,7 +5,7 @@ $(TargetFrameworks);net481 false true - enable + Exe @@ -26,12 +26,13 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -43,8 +44,8 @@ - + diff --git a/src/CommonUtilities.Registry/test/CompilerHelpers/Attributes.cs b/src/CommonUtilities.Registry/test/CompilerHelpers/Attributes.cs new file mode 100644 index 0000000..e1dc9a4 --- /dev/null +++ b/src/CommonUtilities.Registry/test/CompilerHelpers/Attributes.cs @@ -0,0 +1,45 @@ +#if !NET5_0_OR_GREATER +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +namespace System.Runtime.Versioning; + +/// +/// Base type for all platform-specific API attributes. +/// + +internal abstract class OSPlatformAttribute(string platformName) : Attribute +{ + public string PlatformName { get; } = platformName; +} + +/// +/// Records the platform that the project targeted. +/// +[AttributeUsage(AttributeTargets.Assembly)] +internal sealed class TargetPlatformAttribute(string platformName) : OSPlatformAttribute(platformName); + +/// +/// Records the operating system (and minimum version) that supports an API. Multiple attributes can be +/// applied to indicate support on multiple operating systems. +/// +/// +/// Callers can apply a +/// or use guards to prevent calls to APIs on unsupported operating systems. +/// +/// A given platform should only be specified once. +/// +[AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Class | + AttributeTargets.Constructor | + AttributeTargets.Enum | + AttributeTargets.Event | + AttributeTargets.Field | + AttributeTargets.Interface | + AttributeTargets.Method | + AttributeTargets.Module | + AttributeTargets.Property | + AttributeTargets.Struct, + AllowMultiple = true, Inherited = false)] +internal sealed class SupportedOSPlatformAttribute(string platformName) : OSPlatformAttribute(platformName); +// ReSharper restore InconsistentNaming +#endif \ No newline at end of file diff --git a/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs b/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs index fa8b58d..905ed32 100644 --- a/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs +++ b/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs @@ -20,7 +20,7 @@ public abstract class RegistryKeyExtensionsTestBase public async Task AwaitRegKeyChange() { using var test = CreateTestKey(); - var changeWatcherTask = test.Key.WaitForChangeAsync(); + var changeWatcherTask = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.False(changeWatcherTask.IsCompleted); test.Key.SetValue("a", "b"); await changeWatcherTask; @@ -32,13 +32,13 @@ public async Task AwaitRegKeyChange() public async Task AwaitRegKeyChange_CreateOtherUnrelatedKey_DoesNotNotify() { using var test = CreateTestKey(); - var changeWatcherTask = test.Key.WaitForChangeAsync(); + var changeWatcherTask = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.False(changeWatcherTask.IsCompleted); using var other = CreateTestKey(); other.Key.CreateSubKey("otherSub"); - var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay)); + var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken)); Assert.NotSame(changeWatcherTask, completedTask); } @@ -46,12 +46,12 @@ public async Task AwaitRegKeyChange_CreateOtherUnrelatedKey_DoesNotNotify() public async Task AwaitRegKeyChange_SubkeyFilterDoesNotNotifyOnValueChanges() { using var test = CreateTestKey(); - var changeWatcherTask = test.Key.WaitForChangeAsync(change: RegistryChangeNotificationFilters.Subkey); + var changeWatcherTask = test.Key.WaitForChangeAsync(change: RegistryChangeNotificationFilters.Subkey, cancellationToken: TestContext.Current.CancellationToken); Assert.False(changeWatcherTask.IsCompleted); test.Key.SetValue("a", "b"); - var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay)); + var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken)); Assert.NotSame(changeWatcherTask, completedTask); } @@ -63,8 +63,8 @@ public async Task AwaitRegKeyChange_TwoAtOnce_SameKeyHandle() try { - var changeWatcherTask1 = test.Key.WaitForChangeAsync(); - var changeWatcherTask2 = test.Key.WaitForChangeAsync(); + var changeWatcherTask1 = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken); + var changeWatcherTask2 = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.False(changeWatcherTask1.IsCompleted); Assert.False(changeWatcherTask2.IsCompleted); @@ -89,7 +89,7 @@ public async Task AwaitRegKeyChange_NoChange() Assert.False(changeWatcherTask.IsCompleted); // Give a bit of time to confirm the task will not complete. - var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay)); + var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken)); Assert.NotSame(changeWatcherTask, completedTask); } @@ -145,7 +145,7 @@ public async Task AwaitRegKeyChange_SelfDeleted_AlwaysNotifies(RegistryChangeNot try { - var changeWatcherTask = subKey.WaitForChangeAsync(watchSubtree: false ,change: filter); + var changeWatcherTask = subKey.WaitForChangeAsync(watchSubtree: false ,change: filter, TestContext.Current.CancellationToken); test.Key.DeleteKey(GetRegistryKeySubName(subKey.Name), false); await changeWatcherTask; } @@ -212,7 +212,7 @@ public async Task AwaitRegKeyChange_SubKeyDeleted_ValueFilterDoesNotNotify(bool var changeWatcherTask = test.Key.WaitForChangeAsync(watchSubtree: watchSubtree, RegistryChangeNotificationFilters.Value, cancellationToken: test.FinishedToken); test.Key.DeleteKey(GetRegistryKeySubName(subKey.Name), false); - var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay)); + var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken)); Assert.NotSame(changeWatcherTask, completedTask); } finally @@ -252,7 +252,7 @@ public async Task AwaitRegKeyChange_SubSubKeyDeleted_NoWatchSubtree() var changeWatcherTask = test.Key.WaitForChangeAsync(watchSubtree: false, cancellationToken: test.FinishedToken); test.Key.DeleteKey("sub\\subsub", false); - var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay)); + var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken)); Assert.NotSame(changeWatcherTask, completedTask); } finally @@ -271,7 +271,8 @@ public async Task AwaitRegKeyChange_ParentKeyDeletedWhileAwaiting() try { // Only watch for value changes, not tree changes, so we don't notify - var changeWatcherTask = subKey.WaitForChangeAsync(watchSubtree: false, RegistryChangeNotificationFilters.Value); + var changeWatcherTask = subKey.WaitForChangeAsync(watchSubtree: false, + RegistryChangeNotificationFilters.Value, cancellationToken: TestContext.Current.CancellationToken); // Delete the parent key test.Key.DeleteKey(string.Empty, true); @@ -300,7 +301,7 @@ public async Task AwaitRegKeyChange_NoWatchSubtree() // We do not expect changes to sub-keys to complete the task, so give a bit of time to confirm // the task doesn't complete. - var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay)); + var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken)); Assert.NotSame(changeWatcherTask, completedTask); } finally @@ -335,7 +336,7 @@ public async Task AwaitRegKeyChange_KeyDisposedWhileWatching() Task watchingTask; using (var test = CreateTestKey()) { - watchingTask = test.Key.WaitForChangeAsync(); + watchingTask = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken); } // We expect the task to quietly complete (without throwing any exception). @@ -382,7 +383,7 @@ public async Task AwaitRegKeyChange_CallingThreadDestroyed() thread.Join(); // Verify that the watching task is still watching. - var completedTask = await Task.WhenAny(watchingTask, Task.Delay(AsyncDelay)); + var completedTask = await Task.WhenAny(watchingTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken)); Assert.NotSame(watchingTask, completedTask); test.CreateSubKey().Dispose(); diff --git a/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs b/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs index ad03bf7..05c0196 100644 --- a/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs +++ b/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs @@ -1,10 +1,12 @@ #if Windows +using System.Runtime.Versioning; using AnakinRaW.CommonUtilities.Registry.Windows; namespace AnakinRaW.CommonUtilities.Registry.Test.Extensions; // ReSharper disable once UnusedMember.Global +[SupportedOSPlatform("windows")] public class WindowsKeyExtensionsTest : RegistryKeyExtensionsTestBase { protected override RegKeyTest CreateTestKey() diff --git a/src/CommonUtilities.Registry/test/RegistryKey_DeleteKey_Recursive.cs b/src/CommonUtilities.Registry/test/RegistryKey_DeleteKey_Recursive.cs index 8aba104..c25eca3 100644 --- a/src/CommonUtilities.Registry/test/RegistryKey_DeleteKey_Recursive.cs +++ b/src/CommonUtilities.Registry/test/RegistryKey_DeleteKey_Recursive.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Extensions; using Xunit; namespace AnakinRaW.CommonUtilities.Registry.Test; diff --git a/src/CommonUtilities.Registry/test/RegistryKey_TypeLimit.cs b/src/CommonUtilities.Registry/test/RegistryKey_TypeLimit.cs index 2938a29..c8811ac 100644 --- a/src/CommonUtilities.Registry/test/RegistryKey_TypeLimit.cs +++ b/src/CommonUtilities.Registry/test/RegistryKey_TypeLimit.cs @@ -1,5 +1,5 @@ using System; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Extensions; using Xunit; namespace AnakinRaW.CommonUtilities.Registry.Test; @@ -17,13 +17,14 @@ public void SetValue_InvalidDataTypes_WindowsCompatibility() // Should throw because only String[] (REG_MULTI_SZ) and byte[] (REG_BINARY) are supported. // RegistryKey.SetValue does not support arrays of type UInt32[]. - AssertExtensions.Throws(null, () => TestRegistryKey.SetValue("IntArray", value: new[] { 1, 2, 3 })); + AssertExtensions.Throws(null, () => TestRegistryKey.SetValue("IntArray", value: (int[])[1, 2, 3 + ])); } else { TestRegistryKey.SetValue("StringArr", value: new string[1]); - Assert.Equal([null!], TestRegistryKey.GetValue("StringArr")!); - TestRegistryKey.SetValue("IntArray", value: new[] { 1, 2, 3 }); + Assert.Equal((string[])[null!], TestRegistryKey.GetValue("StringArr")!); + TestRegistryKey.SetValue("IntArray", value: (int[])[1, 2, 3]); Assert.Equal([1, 2, 3], TestRegistryKey.GetValue("IntArray")!); } } diff --git a/src/CommonUtilities.Registry/test/TestData.cs b/src/CommonUtilities.Registry/test/TestData.cs index 953febf..cdbcb48 100644 --- a/src/CommonUtilities.Registry/test/TestData.cs +++ b/src/CommonUtilities.Registry/test/TestData.cs @@ -32,10 +32,10 @@ static TestData() ["Test_06", (uint)rand.Next(0, int.MaxValue)], ["Test_07", (long)rand.Next(int.MinValue, int.MaxValue)], ["Test_08", (ulong)rand.Next(0, int.MaxValue)], - ["Test_09", new decimal(((double)decimal.MaxValue) * rand.NextDouble())], - ["Test_10", new decimal(((double)decimal.MinValue) * rand.NextDouble())], - ["Test_11", new decimal(((double)decimal.MinValue) * rand.NextDouble())], - ["Test_12", new decimal(((double)decimal.MaxValue) * rand.NextDouble())], + ["Test_09", new decimal((double)decimal.MaxValue * rand.NextDouble())], + ["Test_10", new decimal((double)decimal.MinValue * rand.NextDouble())], + ["Test_11", new decimal((double)decimal.MinValue * rand.NextDouble())], + ["Test_12", new decimal((double)decimal.MaxValue * rand.NextDouble())], ["Test_13", int.MaxValue *rand.NextDouble()], ["Test_14", int.MinValue * rand.NextDouble()], ["Test_15", int.MaxValue * (float)rand.NextDouble()], diff --git a/src/CommonUtilities.Registry/test/WindowsRegistryTests.cs b/src/CommonUtilities.Registry/test/WindowsRegistryTests.cs index c78fc9a..1fb98f5 100644 --- a/src/CommonUtilities.Registry/test/WindowsRegistryTests.cs +++ b/src/CommonUtilities.Registry/test/WindowsRegistryTests.cs @@ -3,8 +3,8 @@ using AnakinRaW.CommonUtilities.Registry.Windows; using Microsoft.Win32; using Xunit; -using AnakinRaW.CommonUtilities.Testing; using System; +using AnakinRaW.CommonUtilities.Testing.Attributes; #if NET diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index 8234f1f..285e852 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -1,15 +1,17 @@ - + + CommonUtilities.SimplePipeline + AnakinRaW.CommonUtilities.SimplePipeline Implements pipeline runners to run steps synchronized or in parallel. - netstandard2.0;netstandard2.1;net10.0 - enable - true + true + netstandard2.0;netstandard2.1;net10.0 AnakinRaW.CommonUtilities.SimplePipeline AnakinRaW.CommonUtilities.SimplePipeline + en diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index 4cef3d5..5f8d439 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -1,10 +1,11 @@ - + net10.0;net8.0 $(TargetFrameworks);net481 false true + Exe @@ -12,20 +13,16 @@ AnakinRaW.CommonUtilities.SimplePipeline.Test - - enable - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -37,8 +34,8 @@ - + diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs index e899338..2dfeff4 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs @@ -16,7 +16,7 @@ public async Task RunAsync_DelayedAdd() var s1 = new TestStep(_ => { - Task.Delay(3000).Wait(); + Task.Delay(3000, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); tcs.SetResult(0); }, ServiceProvider); @@ -28,7 +28,7 @@ public async Task RunAsync_DelayedAdd() var pipeline = CreateConsumerPipeline(ValueFunction()); - await pipeline.RunAsync(); + await pipeline.RunAsync(TestContext.Current.CancellationToken); Assert.True(s2Run); @@ -61,7 +61,7 @@ public async Task RunAsync_DelayedAdd_PrepareFails(bool failFast) var pipeline = CreateConsumerPipeline(ValueFunction(), failFast); - var task = Assert.ThrowsAsync(async () => await pipeline.RunAsync()); + var task = Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); if (failFast) await task; @@ -118,7 +118,7 @@ public async Task RunAsync_PrepareCancelled() var s1 = new TestStep(_ => { - Task.Delay(3000).Wait(); + Task.Delay(3000, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); tcs.SetResult(0); }, ServiceProvider); @@ -141,7 +141,7 @@ async IAsyncEnumerable ValueFunction() yield return s1; await tcs.Task; cts.Cancel(); - await Task.Delay(1000); + await Task.Delay(1000, TestContext.Current.CancellationToken); yield return s2; } } diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs index be3a333..2458253 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs @@ -7,7 +7,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -public abstract class PipelineTest : CommonTestBase +public abstract class PipelineTest : TestBaseWithServiceProvider { protected abstract Pipeline CreatePipeline(IList steps); @@ -30,7 +30,7 @@ public async Task Dispose() Assert.True(pipeline.IsDisposed); await Assert.ThrowsAsync(pipeline.PrepareAsync); - await Assert.ThrowsAsync(async () => await pipeline.RunAsync()); + await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); } [Fact] @@ -40,8 +40,8 @@ public async Task Run_RunMultipleTimesDoesNotPrepareAgain_StepRunOnlyOnce() var s = new TestStep(_ => { counter++; }, ServiceProvider); var pipeline = CreatePipeline([s]); - await pipeline.RunAsync(); - await pipeline.RunAsync(); + await pipeline.RunAsync(TestContext.Current.CancellationToken); + await pipeline.RunAsync(TestContext.Current.CancellationToken); Assert.Equal(1, counter); } @@ -54,7 +54,7 @@ public async Task PrepareThenRun() var pipeline = CreatePipeline([s]); await pipeline.PrepareAsync(); - await pipeline.RunAsync(); + await pipeline.RunAsync(TestContext.Current.CancellationToken); Assert.Equal(1, counter); } @@ -82,7 +82,7 @@ public async Task Prepare_Disposed_ThrowsObjectDisposedException() pipeline.Dispose(); await Assert.ThrowsAsync(pipeline.PrepareAsync); - await Assert.ThrowsAsync(async () => await pipeline.RunAsync()); + await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); Assert.Equal(0, counter); Assert.False(pipeline.PipelineFailed); @@ -98,7 +98,7 @@ public async Task Run_Disposed_ThrowsObjectDisposedException() await pipeline.PrepareAsync(); pipeline.Dispose(); - await Assert.ThrowsAsync(async () => await pipeline.RunAsync()); + await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); Assert.Equal(0, counter); Assert.False(pipeline.PipelineFailed); } diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs index 2e88024..67f3169 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs @@ -36,7 +36,7 @@ public async Task RunAsync_RunsInSequence() var pipeline = CreatePipeline([s1, s2], true); - await pipeline.RunAsync(); + await pipeline.RunAsync(TestContext.Current.CancellationToken); Assert.Equal("ab", sb.ToString()); Assert.False(pipeline.PipelineFailed); @@ -54,7 +54,7 @@ public async Task RunAsync_WithError_FailFastBehavior_Throws(bool failFast, stri var pipeline = CreatePipeline([s1, s2], failFast); - var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync()); + var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); Assert.Equal("Step 'TestStep' failed with error: Test", e.Message); Assert.Equal(result, sb.ToString()); Assert.True(pipeline.PipelineFailed); diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTest.cs index ce7edff..d0ca0b3 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTest.cs @@ -15,7 +15,7 @@ public async Task RunAsync_EmptyPipeline() { var pipeline = CreatePipeline([], true); - await pipeline.RunAsync(); + await pipeline.RunAsync(TestContext.Current.CancellationToken); Assert.False(pipeline.PipelineFailed); } @@ -37,7 +37,7 @@ public async Task RunAsync_AllStepsExecuted(bool prepare) await pipeline.PrepareAsync(); // Double prepare should have no effect } - await pipeline.RunAsync(); + await pipeline.RunAsync(TestContext.Current.CancellationToken); Assert.Equal(2, runCounter); Assert.False(pipeline.PipelineFailed); } @@ -49,7 +49,7 @@ public async Task RunAsync_WithError_Throws() var pipeline = CreatePipeline([s1], true); - var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync()); + var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); Assert.Equal("Step 'TestStep' failed with error: Test", e.Message); Assert.True(pipeline.PipelineFailed); } @@ -63,7 +63,7 @@ public async Task RunAsync_WithError_FailSlow_Throws() var pipeline = CreatePipeline([s1, s2], false); - var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync()); + var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); Assert.Equal("Step 'TestStep' failed with error: Test", e.Message); Assert.True(pipeline.PipelineFailed); Assert.True(ran); diff --git a/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs b/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs index 099142d..74885e6 100644 --- a/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs @@ -31,7 +31,7 @@ protected override TestInfoClass CreateCustomProgressInfo(TestProgressStep : CommonTestBase where T : ITestInfo, new() +public abstract class AggregatedProgressReporterTestBase : TestBaseWithServiceProvider where T : ITestInfo, new() { private readonly TestProgressReporter _internalReporter = new(); diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerStepRunnerTest.cs index 4fc03ba..593ebec 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerStepRunnerTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerStepRunnerTest.cs @@ -89,7 +89,7 @@ public void Run_AddDelayed() runner.AddStep(s3); Task.Delay(1000); runner.Finish(); - }); + }, TestContext.Current.CancellationToken); runner.Wait(); @@ -120,7 +120,7 @@ public async Task Run_AddDelayed_Await() runner.AddStep(s3); runner.Finish(); - }).Forget(); + }, TestContext.Current.CancellationToken).Forget(); await runTask; // Should not block diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTest.cs index 68d5172..3c56fdd 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTest.cs @@ -36,7 +36,7 @@ public async Task RunAsync_Cancelled() var b = new ManualResetEvent(false); - StepRunnerErrorEventArgs? error = null!; + StepRunnerErrorEventArgs? error = null; runner.Error += (_, e) => { error = e; diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs index cf12592..6cca680 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs @@ -8,7 +8,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; -public abstract class StepRunnerTestBase : CommonTestBase where T : class, IStepRunner +public abstract class StepRunnerTestBase : TestBaseWithServiceProvider where T : class, IStepRunner { public abstract bool PreservesStepExecutionOrder { get; } @@ -111,7 +111,7 @@ public async Task RunAsync() var step1 = new TestStep(_ => { ranList.Add("Step1"); - tsc.Wait(); + tsc.Wait(TestContext.Current.CancellationToken); }, ServiceProvider); var step2 = new TestStep(_ => ranList.Add("Step2"), ServiceProvider); @@ -148,7 +148,7 @@ public async Task RunAsync_Cancellation() var cts = new CancellationTokenSource(); var step1 = new TestStep(_ => { - Task.Delay(1000).Wait(); + Task.Delay(1000, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); ranList.Add("Step1"); cts.Cancel(); @@ -174,7 +174,7 @@ public async Task RunAsync_StopRunner_ShouldStopExecution() { var runner = CreateStepRunner(true); - StepRunnerErrorEventArgs? args = null!; + StepRunnerErrorEventArgs? args = null; runner.Error += (_, e) => { args = e; @@ -184,7 +184,7 @@ public async Task RunAsync_StopRunner_ShouldStopExecution() var cts = new CancellationTokenSource(); var step1 = new TestStep(_ => { - Task.Delay(1000).Wait(); + Task.Delay(1000, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); ranList.Add("Step1"); cts.Cancel(); diff --git a/src/CommonUtilities.SimplePipeline/test/StepErrorEventArgsTest.cs b/src/CommonUtilities.SimplePipeline/test/StepErrorEventArgsTest.cs index 1a37b4d..6df7c6e 100644 --- a/src/CommonUtilities.SimplePipeline/test/StepErrorEventArgsTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/StepErrorEventArgsTest.cs @@ -4,7 +4,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test; -public class StepErrorEventArgsTest : CommonTestBase +public class StepErrorEventArgsTest : TestBaseWithServiceProvider { [Fact] public void Cancel() diff --git a/src/CommonUtilities.SimplePipeline/test/StepFailureExceptionTests.cs b/src/CommonUtilities.SimplePipeline/test/StepFailureExceptionTests.cs index a765b5a..2aa3a98 100644 --- a/src/CommonUtilities.SimplePipeline/test/StepFailureExceptionTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/StepFailureExceptionTests.cs @@ -5,7 +5,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test; -public class StepFailureExceptionTests : CommonTestBase +public class StepFailureExceptionTests : TestBaseWithServiceProvider { [Fact] public void Ctor_WithNullFailedSteps_ThrowsArgumentNullException() diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs index 5f4f757..6196163 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs @@ -5,7 +5,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class PipelineStepTest : CommonTestBase +public class PipelineStepTest : TestBaseWithServiceProvider { [Fact] public void Ctor_NullArgs_Throws() diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs index 958ee1f..f0edc53 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs @@ -7,7 +7,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class RunPipelineStepTest : CommonTestBase +public class RunPipelineStepTest : TestBaseWithServiceProvider { private class DelegatePipeline(Func action, IServiceProvider serviceProvider) : Pipeline(serviceProvider) { diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/SynchronizedStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/SynchronizedStepTest.cs index bb2c144..51142dc 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/SynchronizedStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/SynchronizedStepTest.cs @@ -6,7 +6,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class SynchronizedStepTest : CommonTestBase +public class SynchronizedStepTest : TestBaseWithServiceProvider { [Fact] public void Wait_ThrowsTimeoutException() @@ -60,7 +60,7 @@ public void Wait() flag = true; }, ServiceProvider); - Task.Run(() => step.Run(CancellationToken.None)).Forget(); + Task.Run(() => step.Run(CancellationToken.None), TestContext.Current.CancellationToken).Forget(); step.Wait(); diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs index 019f5b6..972f1d3 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs @@ -7,7 +7,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class WaitStepTest : CommonTestBase +public class WaitStepTest : TestBaseWithServiceProvider { [Fact] public void Wait() diff --git a/src/CommonUtilities.SimplePipeline/test/TestStep.cs b/src/CommonUtilities.SimplePipeline/test/TestStep.cs index bfef8bb..dcb44d0 100644 --- a/src/CommonUtilities.SimplePipeline/test/TestStep.cs +++ b/src/CommonUtilities.SimplePipeline/test/TestStep.cs @@ -81,10 +81,7 @@ private bool Equals(TestInfoClass other) public override int GetHashCode() { - unchecked - { - return (Progress.GetHashCode() * 397) ^ Aggregated.GetHashCode(); - } + return HashCode.Combine(Progress, Aggregated); } } diff --git a/src/CommonUtilities.Testing/Attributes/PlatformSpecificFactAttribute.cs b/src/CommonUtilities.Testing/Attributes/PlatformSpecificFactAttribute.cs new file mode 100644 index 0000000..44d8e24 --- /dev/null +++ b/src/CommonUtilities.Testing/Attributes/PlatformSpecificFactAttribute.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Testing.Attributes; + +/// +/// Test attribute that specifies the test should only run on specific platforms. +/// +/// +/// This attribute allows you to define platform-specific tests by specifying the target platforms +/// using . If the current platform does not match any of the specified +/// platforms, the test will be skipped with an appropriate message. +/// +public sealed class PlatformSpecificFactAttribute : FactAttribute +{ + /// + /// Initializes a new instance of the class with the specified target platforms. + /// + /// + /// An array of values that specify the platforms on which the test should run. + /// + /// + /// If the current platform does not match any of the specified , the test will be skipped + /// with a message indicating that the test execution is not supported on the current platform. + /// + public PlatformSpecificFactAttribute(params TestPlatformIdentifier[] platformIds) + { + var platforms = platformIds.Select(targetPlatform => OSPlatform.Create(Enum.GetName(typeof(TestPlatformIdentifier), targetPlatform)!.ToUpper())); + var platformMatches = platforms.Any(RuntimeInformation.IsOSPlatform); + + if (!platformMatches) + Skip = "Test execution is not supported on the current platform"; + } +} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/Attributes/PlatformSpecificTheoryAttribute.cs b/src/CommonUtilities.Testing/Attributes/PlatformSpecificTheoryAttribute.cs new file mode 100644 index 0000000..8d9ed4a --- /dev/null +++ b/src/CommonUtilities.Testing/Attributes/PlatformSpecificTheoryAttribute.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Testing.Attributes; + +/// +/// A theory attribute that specifies the test should only run on specific platforms. +/// +/// +/// This attribute allows you to define platform-specific tests by specifying the target platforms +/// using . If the current platform does not match any of the specified +/// platforms, the test will be skipped with an appropriate message. +/// +public sealed class PlatformSpecificTheoryAttribute : TheoryAttribute +{ + /// + /// Initializes a new instance of the class with the specified target platforms. + /// + /// + /// An array of values that specify the platforms on which the test should run. + /// + /// + /// If the current platform does not match any of the specified , the test will be skipped + /// with a message indicating that the test execution is not supported on the current platform. + /// + public PlatformSpecificTheoryAttribute(params TestPlatformIdentifier[] platformIds) + { + var platforms = platformIds.Select(targetPlatform => OSPlatform.Create(Enum.GetName(typeof(TestPlatformIdentifier), targetPlatform)!.ToUpper())); + var platformMatches = platforms.Any(RuntimeInformation.IsOSPlatform); + + if (!platformMatches) + Skip = "Test execution is not supported on the current platform"; + } +} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/Attributes/TestPlatformIdentifier.cs b/src/CommonUtilities.Testing/Attributes/TestPlatformIdentifier.cs new file mode 100644 index 0000000..69e23b4 --- /dev/null +++ b/src/CommonUtilities.Testing/Attributes/TestPlatformIdentifier.cs @@ -0,0 +1,23 @@ +using System; + +namespace AnakinRaW.CommonUtilities.Testing.Attributes; + +/// +/// Represents identifiers for test platforms used to specify platform-specific test execution. +/// +/// +/// This enumeration is used in conjunction with attributes like +/// and to define tests that should only run on specific platforms. +/// +[Flags] +public enum TestPlatformIdentifier +{ + /// + /// Represents the Windows platform. + /// + Windows = 1, + /// + /// Represents a Linux platform. + /// + Linux = 2, +} \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/Collections/CollectionsTestSuite.cs b/src/CommonUtilities.Testing/Collections/CollectionsTestSuite.cs similarity index 75% rename from src/CommonUtilities.TestingUtilities/Collections/CollectionsTestSuite.cs rename to src/CommonUtilities.Testing/Collections/CollectionsTestSuite.cs index 1fc1d51..5e7cd47 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/CollectionsTestSuite.cs +++ b/src/CommonUtilities.Testing/Collections/CollectionsTestSuite.cs @@ -1,4 +1,8 @@ +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using Xunit; namespace AnakinRaW.CommonUtilities.Testing.Collections; @@ -9,17 +13,45 @@ namespace AnakinRaW.CommonUtilities.Testing.Collections; /// public abstract class CollectionsTestSuite { + /// + /// Represents a set of operations that can modify a collection during testing. + /// [Flags] public enum ModifyOperation { + /// + /// Represents the absence of any modification operation on a collection. + /// None = 0, + /// + /// Represents an operation that adds an element from a collection. + /// Add = 1, + /// + /// Represents an operation that inserts an element from a collection. + /// Insert = 2, + /// + /// Represents an operation that overwrites an existing element in a collection. + /// Overwrite = 4, + /// + /// Represents an operation that removes an element from a collection. + /// Remove = 8, + /// + /// Represents an operation that clears all elements from a collection. + /// Clear = 16 } + /// + /// Provides a collection of valid sizes for testing collections. + /// + /// + /// An of object arrays, where each array contains + /// a single integer representing a valid collection size. + /// public static IEnumerable ValidCollectionSizes() { yield return [0]; @@ -27,6 +59,20 @@ public static IEnumerable ValidCollectionSizes() yield return [75]; } + /// + /// Provides test data for enumerable-related test cases. + /// + /// + /// This method generates a variety of enumerable configurations to test different scenarios, + /// including empty enumerables, enumerables of varying sizes, and enumerables with duplicate or matching elements. + /// + /// + /// A collection of test data, where each element is an array of objects containing the following: + /// - The size of the original collection. + /// - The size of the enumerable to be tested. + /// - The number of matching elements between the original collection and the enumerable. + /// - The number of duplicate elements in the enumerable. + /// public static IEnumerable GetEnumerableTestData() { foreach (var collectionSizeArray in ValidCollectionSizes()) @@ -131,7 +177,7 @@ protected IEnumerable CreateList(IEnumerable? enumerableToMatchTo, int cou list.Add(toAdd); } - // Validate that the Enumerable fits the guidelines as expected + // ValidateAsync that the Enumerable fits the guidelines as expected Debug.Assert(list.Count == count); if (match != null) { diff --git a/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs b/src/CommonUtilities.Testing/Collections/ICollectionTestSuite.cs similarity index 85% rename from src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs rename to src/CommonUtilities.Testing/Collections/ICollectionTestSuite.cs index 104211c..a990058 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/ICollectionTestSuite.cs +++ b/src/CommonUtilities.Testing/Collections/ICollectionTestSuite.cs @@ -1,5 +1,11 @@ +using AnakinRaW.CommonUtilities.Testing.Extensions; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; +using Xunit; namespace AnakinRaW.CommonUtilities.Testing.Collections; @@ -12,20 +18,80 @@ namespace AnakinRaW.CommonUtilities.Testing.Collections; [SuppressMessage("ReSharper", "InconsistentNaming")] public abstract class ICollectionTestSuite : IEnumerableTestSuite { + /// + /// Gets the of the exception that is expected to be thrown + /// when attempting to copy elements of the collection to an array with a starting index + /// larger than the array's length. + /// + /// + /// By default, this property returns . + /// Override this property in derived classes to specify a different exception type + /// if the behavior differs. + /// protected virtual Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentException); + /// + /// Gets a collection of values that are considered invalid for the test suite. + /// + /// + /// These values are used in various test cases to validate the behavior of the collection + /// when handling invalid inputs. The specific definition of "invalid" depends on the context + /// of the test suite and the type parameter . + /// protected virtual IEnumerable InvalidValues => []; + /// + /// Gets a value indicating whether the default value of the type is allowed + /// to be added to the collection. + /// + /// + /// This property determines whether operations involving the default value of + /// (e.g., adding or checking for the default value) are valid within the collection. + /// + /// + /// if the default value of is allowed in the collection; otherwise, . + /// protected virtual bool DefaultValueAllowed => true; + /// + /// Gets a value indicating whether the Add, Remove, and Clear operations + /// are expected to throw a in the collection. + /// + /// + /// if the Add, Remove, and Clear operations + /// are not supported and should throw a ; otherwise, . + /// protected virtual bool AddRemoveClear_ThrowsNotSupported => false; + /// + /// Indicates whether the collection supports storing duplicate values. + /// + /// + /// if the collection allows multiple instances of the same value; otherwise, . + /// protected virtual bool DuplicateValuesAllowed => true; + /// + /// Gets a value indicating whether the collection is read-only. + /// + /// + /// A read-only collection does not allow the addition, removal, or modification of elements + /// after the collection is created. This property can be overridden to specify whether the + /// collection is read-only in derived classes. + /// + /// + /// if the collection is read-only; otherwise, . + /// protected virtual bool IsReadOnly => false; - protected virtual bool IsReadOnly_ValidityValue => IsReadOnly; - + /// + /// Gets a value indicating whether an exception is thrown when attempting to use the default value + /// in a collection where default values are not allowed. + /// + /// + /// if an exception is thrown when the default value is used in such a collection; + /// otherwise, . + /// protected virtual bool DefaultValueWhenNotAllowed_Throws => true; /// @@ -76,6 +142,7 @@ protected override IEnumerable GetModifyEnumerables(ModifyOper } } + /// protected override IEnumerable GenericIEnumerableFactory(int count) { return GenericICollectionFactory(count); @@ -93,6 +160,11 @@ protected virtual ICollection GenericICollectionFactory(int count) return collection; } + /// + /// Adds a specified number of items to the given collection. + /// + /// The collection to which items will be added. + /// The number of items to add to the collection. protected virtual void AddToCollection(ICollection collection, int numberOfItemsToAdd) { var seed = 9600; @@ -106,17 +178,7 @@ protected virtual void AddToCollection(ICollection collection, int numberOfIt } } - #region IsReadOnly - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void ICollection_Generic_IsReadOnly_Validity(int count) - { - var collection = GenericICollectionFactory(count); - Assert.Equal(IsReadOnly_ValidityValue, collection.IsReadOnly); - } - - #endregion +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member #region Count @@ -241,7 +303,7 @@ public void ICollection_Generic_Add_AfterRemovingAnyValue(int count) collection.Add(toAdd); items.Add(toAdd); - CollectionAsserts.EqualUnordered(items, collection); + Assert.EqualUnordered(items, collection); } } @@ -604,7 +666,7 @@ public void ICollection_Generic_Remove_NonDefaultValueContainedInCollection(int { var seed = count * 251; var collection = GenericICollectionFactory(count); - var value = CreateT(seed++); + var value = CreateT(++seed); if (!collection.Contains(value)) { collection.Add(value); @@ -623,7 +685,7 @@ public void ICollection_Generic_Remove_ValueThatExistsTwiceInCollection(int coun { var seed = count * 90; var collection = GenericICollectionFactory(count); - var value = CreateT(seed++); + var value = CreateT(++seed); collection.Add(value); collection.Add(value); count += 2; @@ -671,4 +733,6 @@ public void ICollection_Generic_Remove_DefaultValueWhenNotAllowed(int count) } #endregion + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/Collections/IEnumerableTestSuite.cs b/src/CommonUtilities.Testing/Collections/IEnumerableTestSuite.cs similarity index 95% rename from src/CommonUtilities.TestingUtilities/Collections/IEnumerableTestSuite.cs rename to src/CommonUtilities.Testing/Collections/IEnumerableTestSuite.cs index 58993a7..63b9cee 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/IEnumerableTestSuite.cs +++ b/src/CommonUtilities.Testing/Collections/IEnumerableTestSuite.cs @@ -1,5 +1,9 @@ +using System; using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Xunit; namespace AnakinRaW.CommonUtilities.Testing.Collections; @@ -19,7 +23,13 @@ public abstract class IEnumerableTestSuite : CollectionsTestSuite /// protected enum EnumerableOrder { + /// + /// Specifies that the enumerable returns in an unspecified order. + /// Unspecified, + /// + /// Specifies that the enumerable returns sequential. + /// Sequential } @@ -117,8 +127,27 @@ protected enum EnumerableOrder /// protected virtual bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException; + /// + /// Gets the set of values that represent modifications to a collection + /// which are expected to throw an when performed during enumeration. + /// + /// + /// This property defines the operations that are not allowed to be performed on a collection + /// while it is being enumerated. By default, it includes , + /// , , + /// , and . + /// protected virtual ModifyOperation ModifyEnumeratorThrows => ModifyOperation.Add | ModifyOperation.Insert | ModifyOperation.Overwrite | ModifyOperation.Remove | ModifyOperation.Clear; + /// + /// Gets a value indicating the types of modification operations that are allowed on an enumerator + /// during enumeration without causing exceptions. + /// + /// + /// This property specifies the set of flags that represent + /// the operations permitted on the enumerator while it is being enumerated. By default, no + /// modifications are allowed, as indicated by . + /// protected virtual ModifyOperation ModifyEnumeratorAllowed => ModifyOperation.None; /// @@ -272,6 +301,7 @@ private void VerifyEnumerator(IEnumerator enumerator, T[] expectedItems, int } } +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member #region GetEnumerator() @@ -696,6 +726,7 @@ public void IEnumerable_NonGeneric_Enumerator_Current_ReturnsSameValueOnRepeated [MemberData(nameof(ValidCollectionSizes))] public void IEnumerable_NonGeneric_Enumerator_Current_ReturnsSameObjectsOnDifferentEnumerators(int count) { +#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. // Ensures that the elements returned from enumeration are exactly the same collection of // elements returned from a previous enumeration var enumerable = NonGenericIEnumerableFactory(count); @@ -709,6 +740,7 @@ public void IEnumerable_NonGeneric_Enumerator_Current_ReturnsSameObjectsOnDiffer Assert.Equal(firstValues.Count, secondValues.Count); foreach (var key in firstValues.Keys) Assert.Equal(firstValues[key], secondValues[key]); +#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. } [Theory] @@ -1151,4 +1183,6 @@ public void IEnumerable_Generic_Enumerator_Reset_ModifiedAfterEnumeration_Succee } #endregion + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/Collections/IListTestSuite.cs b/src/CommonUtilities.Testing/Collections/IListTestSuite.cs similarity index 96% rename from src/CommonUtilities.TestingUtilities/Collections/IListTestSuite.cs rename to src/CommonUtilities.Testing/Collections/IListTestSuite.cs index b3bbc1d..67c45ee 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/IListTestSuite.cs +++ b/src/CommonUtilities.Testing/Collections/IListTestSuite.cs @@ -1,5 +1,9 @@ +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; +using Xunit; namespace AnakinRaW.CommonUtilities.Testing.Collections; @@ -13,6 +17,15 @@ namespace AnakinRaW.CommonUtilities.Testing.Collections; [SuppressMessage("ReSharper", "InconsistentNaming")] public abstract class IListTestSuite : ICollectionTestSuite { + /// + /// Gets the of the exception that is expected to be thrown + /// when accessing or modifying an with an invalid index. + /// + /// + /// By default, this property returns . + /// Derived classes can override this property to specify a different exception type + /// if the behavior of the implementation differs. + /// protected virtual Type IList_Generic_Item_InvalidIndex_ThrowType => typeof(ArgumentOutOfRangeException); /// @@ -90,16 +103,20 @@ protected override IEnumerable GetModifyEnumerables(ModifyOper } } + /// protected override ICollection GenericICollectionFactory() { return GenericIListFactory(); } + /// protected override ICollection GenericICollectionFactory(int count) { return GenericIListFactory(count); } +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + #region Item Getter [Theory] @@ -131,7 +148,7 @@ public void IList_Generic_ItemGet_ValidGetWithinListBounds(int count) return; [MethodImpl(MethodImplOptions.NoInlining)] - void Sink(T _) { } + static void Sink(T _) { } } #endregion @@ -656,20 +673,6 @@ public void IList_Generic_CurrentAtEnd_AfterAdd(int count) } #endregion -} -/// -/// Helper class to provide means to modify an enumerable, which is not the to be tested type. -/// -internal class ModifyEnumerableList(Func createT) : IListTestSuite -{ - protected override T CreateT(int seed) - { - return createT(seed); - } - - protected override IList GenericIListFactory() - { - throw new NotImplementedException(); - } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyCollectionTestSuite.cs b/src/CommonUtilities.Testing/Collections/IReadOnlyCollectionTestSuite.cs similarity index 88% rename from src/CommonUtilities.TestingUtilities/Collections/IReadOnlyCollectionTestSuite.cs rename to src/CommonUtilities.Testing/Collections/IReadOnlyCollectionTestSuite.cs index fe721f7..18607fd 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyCollectionTestSuite.cs +++ b/src/CommonUtilities.Testing/Collections/IReadOnlyCollectionTestSuite.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Xunit; namespace AnakinRaW.CommonUtilities.Testing.Collections; @@ -17,6 +19,7 @@ public abstract class IReadOnlyCollectionTestSuite : IEnumerableTestSuite /// An instance of an that can be used for testing. protected abstract IReadOnlyCollection GenericIReadOnlyCollectionFactory(IEnumerable baseCollection); + /// protected override IEnumerable GenericIEnumerableFactory(int count) { return GenericIReadOnlyCollectionFactory(count); @@ -33,11 +36,14 @@ protected virtual IReadOnlyCollection GenericIReadOnlyCollectionFactory(int c return GenericIReadOnlyCollectionFactory(collection); } + /// protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) { yield break; } +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + #region Count [Theory] @@ -49,4 +55,6 @@ public void ICollection_Generic_Count_Validity(int count) } #endregion + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyListTestSuite.cs b/src/CommonUtilities.Testing/Collections/IReadOnlyListTestSuite.cs similarity index 80% rename from src/CommonUtilities.TestingUtilities/Collections/IReadOnlyListTestSuite.cs rename to src/CommonUtilities.Testing/Collections/IReadOnlyListTestSuite.cs index fb0ef1b..93a9c73 100644 --- a/src/CommonUtilities.TestingUtilities/Collections/IReadOnlyListTestSuite.cs +++ b/src/CommonUtilities.Testing/Collections/IReadOnlyListTestSuite.cs @@ -1,5 +1,9 @@ +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; +using Xunit; namespace AnakinRaW.CommonUtilities.Testing.Collections; @@ -12,6 +16,15 @@ namespace AnakinRaW.CommonUtilities.Testing.Collections; [SuppressMessage("ReSharper", "InconsistentNaming")] public abstract class IReadOnlyListTestSuite : IReadOnlyCollectionTestSuite { + /// + /// Gets the of the exception that is expected to be thrown + /// when accessing or modifying an with an invalid index. + /// + /// + /// By default, this property returns . + /// Derived classes can override this property to specify a different exception type + /// if the behavior of the implementation differs. + /// protected virtual Type IList_Generic_Item_InvalidIndex_ThrowType => typeof(ArgumentOutOfRangeException); /// @@ -31,20 +44,21 @@ protected virtual IReadOnlyList GenericIReadOnlyListFactory(int count) return GenericIReadOnlyListFactory(baseCollection); } + /// protected override IReadOnlyCollection GenericIReadOnlyCollectionFactory(IEnumerable baseCollection) { return GenericIReadOnlyListFactory(baseCollection); } +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + #region FromEnumerable [Theory] [MemberData(nameof(GetEnumerableTestData))] - #pragma warning disable xUnit1026 public void From_IEnumerable(int _, int enumerableLength, int __, int numberOfDuplicateElements) - #pragma warning restore xUnit1026 { - var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); + var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements).ToList(); var list = GenericIReadOnlyListFactory(enumerable); var expected = enumerable.ToList(); @@ -88,8 +102,10 @@ public void IList_Generic_ItemGet_ValidGetWithinListBounds(int count) return; [MethodImpl(MethodImplOptions.NoInlining)] - void Sink(T _) { } + static void Sink(T _) { } } #endregion + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } \ No newline at end of file diff --git a/src/CommonUtilities.Testing/Collections/ModifyEnumerableList.cs b/src/CommonUtilities.Testing/Collections/ModifyEnumerableList.cs new file mode 100644 index 0000000..338fee2 --- /dev/null +++ b/src/CommonUtilities.Testing/Collections/ModifyEnumerableList.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace AnakinRaW.CommonUtilities.Testing.Collections; + +/// +/// Helper class to provide means to modify an enumerable, which is not the to be tested type. +/// +internal class ModifyEnumerableList(Func createT) : IListTestSuite +{ + protected override T CreateT(int seed) + { + return createT(seed); + } + + protected override IList GenericIListFactory() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj b/src/CommonUtilities.Testing/CommonUtilities.Testing.csproj similarity index 54% rename from src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj rename to src/CommonUtilities.Testing/CommonUtilities.Testing.csproj index 2b0d8f8..46b695b 100644 --- a/src/CommonUtilities.TestingUtilities/CommonUtilities.TestingUtilities.csproj +++ b/src/CommonUtilities.Testing/CommonUtilities.Testing.csproj @@ -1,45 +1,50 @@  + - net10.0;net8.0 - $(TargetFrameworks);net481 - enable - enable - false - false + CommonUtilities.Testing AnakinRaW.CommonUtilities.Testing + Provides common utilities for testing projects. + + + + true + false AnakinRaW.CommonUtilities.Testing AnakinRaW.CommonUtilities.Testing + netstandard2.0;netstandard2.1;net10.0 + en + true - xUnit2013 + true + snupkg + true + true + true - + - + - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/src/CommonUtilities.Testing/EqualityComparers/ConstantHashCodeEqualityComparer.cs b/src/CommonUtilities.Testing/EqualityComparers/ConstantHashCodeEqualityComparer.cs new file mode 100644 index 0000000..1d50573 --- /dev/null +++ b/src/CommonUtilities.Testing/EqualityComparers/ConstantHashCodeEqualityComparer.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace AnakinRaW.CommonUtilities.Testing; + +/// +/// Provides an equality comparer for objects of type that always returns a constant hash code. +/// +/// The type of objects to compare. +/// +/// This comparer uses a constant hash code for all objects, which can be useful for testing scenarios where hash code collisions need to be simulated. +/// +public sealed class ConstantHashCodeEqualityComparer(IEqualityComparer comparer) : IEqualityComparer +{ + /// + /// Determines whether the specified objects are equal. + /// + /// + /// This method delegates the equality comparison to the underlying comparer provided during the construction of the . + /// + /// The first object to compare. + /// The second object to compare. + /// if the specified objects are equal; otherwise, . + public bool Equals(T? x, T? y) + { +#pragma warning disable CS8604 // Possible null reference argument. + return comparer.Equals(x, y); +#pragma warning restore CS8604 // Possible null reference argument. + } + + /// + /// Returns a constant hash code for the specified object. + /// + /// + /// This method always returns the same hash code value (42) regardless of the input object. + /// + /// The object for which the hash code is to be generated. + /// A constant hash code value. + public int GetHashCode(T obj) + { + return 42; + } +} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/EqualityComparers/ReferenceEqualityComparer.cs b/src/CommonUtilities.Testing/EqualityComparers/ReferenceEqualityComparer.cs new file mode 100644 index 0000000..c1a9e42 --- /dev/null +++ b/src/CommonUtilities.Testing/EqualityComparers/ReferenceEqualityComparer.cs @@ -0,0 +1,48 @@ +#if !NET5_0_OR_GREATER +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace AnakinRaW.CommonUtilities.Testing; + +/// +/// An that uses reference equality () +/// instead of value equality() when comparing two object instances. +/// +/// +/// The type cannot be instantiated. +/// Instead, use the property to access the singleton instance of this type. +/// +public sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer +{ + /// + /// Gets the singleton instance. + /// + public static ReferenceEqualityComparer Instance { get; } = new(); + + private ReferenceEqualityComparer() + { + } + + /// + /// Determines whether two object references refer to the same object instance. + /// + /// The first object to compare. + /// The second object to compare. + /// + /// if both and + /// refer to the same object instance or if both are ; otherwise, . + /// + public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); + + /// + /// Returns a hash code for the specified object. The returned hash code is based on the object identity, not on the contents of the object. + /// + /// The object for which to retrieve the hash code. + /// A hash code for the identity of . + public int GetHashCode(object? obj) + { + return RuntimeHelpers.GetHashCode(obj!); + } +} +#endif \ No newline at end of file diff --git a/src/CommonUtilities.Testing/Extensions/AssertExtensions.cs b/src/CommonUtilities.Testing/Extensions/AssertExtensions.cs new file mode 100644 index 0000000..00f8182 --- /dev/null +++ b/src/CommonUtilities.Testing/Extensions/AssertExtensions.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Testing.Extensions; + +/// +/// Provides extension methods for the class. +/// +public static class AssertExtensions +{ + private static bool IsNetFramework => RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"); + + extension(Assert) + { + /// + /// Verifies that the specified action does not throw any exception. + /// + /// The type of the result returned by the action. + /// A delegate to the code to be tested. + /// The result of the executed test code. + public static T DoesNotThrowException(Func action) + { + try + { + return action(); + } + catch (Exception e) + { + Assert.Fail($"Expected no exception to be thrown but got '{e.GetType().Name}' instead"); + return default; + } + } + + /// + /// Verifies that the specified action does not throw any exception. + /// + /// A delegate to the code to be tested. + public static void DoesNotThrowException(Action action) + { + Assert.DoesNotThrowException(() => action); + } + + /// + /// Verifies that the specified action throws an exception of the specified and that the exception's parameter name matches the expected value. + /// + /// The type of the exception expected to be thrown. + /// The expected name of the parameter that caused the exception. + /// A delegate to the code expected to throw the exception. + /// The exception that was thrown. + public static T Throws(string? expectedParamName, Action action) where T : ArgumentException + { + var exception = Assert.Throws(action); + Assert.Equal(expectedParamName, exception.ParamName); + return exception; + } + + /// + /// Verifies that the specified action throws an exception of the specified and that the exception's parameter name matches the expected value. + /// + /// The type of the exception expected to be thrown. + /// The expected name of the parameter that caused the exception when executing the test on .NET Core. + /// The expected name of the parameter that caused the exception when executing the test on .NET Framework. + /// A delegate to the code expected to throw the exception. + /// The exception that was thrown. + public static T Throws(string netCoreParamName, string? netFxParamName, Action action) + where T : ArgumentException + { + var exception = Assert.Throws(action); + + if (netFxParamName == null && IsNetFramework) + { + // Param name varies between .NET Framework versions -- skip checking it + return exception; + } + + var expectedParamName = IsNetFramework ? netFxParamName : netCoreParamName; + + Assert.Equal(expectedParamName, exception.ParamName); + return exception; + } + + // From https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/System/Collections/CollectionAsserts.cs + + /// + /// Verifies that two collections contain the same elements, regardless of order. + /// + /// The expected collection. + /// The actual collection. + public static void EqualUnordered(ICollection expected, ICollection actual) + { + Assert.Equal(expected == null, actual == null); + if (expected == null) + return; + + // Lookups are an aggregated collections (enumerable contents), but ordered. + var e = expected.Cast().ToLookup(key => key); + var a = actual!.Cast().ToLookup(key => key); + + // Dictionaries can't handle null keys, which is a possibility + Assert.Equal( + e.Where(kv => kv.Key != null).ToDictionary(g => g.Key, g => g.Count()), + a.Where(kv => kv.Key != null).ToDictionary(g => g.Key, g => g.Count())); + + // Get count of null keys. Returns an empty sequence (and thus a 0 count) if no null key + Assert.Equal(e[null!].Count(), a[null!].Count()); + } + + /// + /// Verifies that two collections contain the same elements, regardless of order. + /// + /// The expected collection. + /// The actual collection. + public static void EqualUnordered(ICollection expected, ICollection actual) + { + Assert.Equal(expected == null, actual == null); + if (expected == null) + return; + + // Lookups are an aggregated collections (enumerable contents), but ordered. + var e = expected.Cast().ToLookup(key => key); + var a = actual!.Cast().ToLookup(key => key); + + // Dictionaries can't handle null keys, which is a possibility + Assert.Equal( + e.Where(kv => kv.Key != null).ToDictionary(g => g.Key, g => g.Count()), + a.Where(kv => kv.Key != null).ToDictionary(g => g.Key, g => g.Count())); + + // Get count of null keys. Returns an empty sequence (and thus a 0 count) if no null key + Assert.Equal(e[null!].Count(), a[null!].Count()); + } + + // Based on https://github.com/dotnet/runtime/blob/main/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Exception.Helpers.cs + + /// + /// Validates the properties of the specified instance against the provided values. + /// + /// The exception instance to validate. + /// The expected inner exception of . Defaults to null. + /// The expected message of . Defaults to null. + /// The expected source of . Defaults to null. + /// The expected stack trace of . Defaults to null. + /// A value indicating whether to validate the property. + public static void Exception(Exception e, + Exception? innerException = null, + string? message = null, + string? source = null, + string? stackTrace = null, + bool validateMessage = true) + { + Assert.Equal(innerException, e.InnerException); + if (validateMessage) + Assert.Equal(message, e.Message); + else + Assert.NotNull(e.Message); + Assert.Equal(source, e.Source); + Assert.Equal(stackTrace, e.StackTrace); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/Extensions/RandomExtensions.cs b/src/CommonUtilities.Testing/Extensions/RandomExtensions.cs new file mode 100644 index 0000000..e45abac --- /dev/null +++ b/src/CommonUtilities.Testing/Extensions/RandomExtensions.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; + +namespace AnakinRaW.CommonUtilities.Extensions; + +/// +/// Provides extension methods for the class. +/// +public static class RandomExtensions +{ + private static readonly Random Random = new(); + private static readonly string AllowedChars = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789!@$?_-"; + + extension(Random) + { + /// + /// Generates a random value. + /// + /// + /// A randomly generated value. + /// + public static bool Bool() + { + return Random.Next() % 2 == 0; + } + + /// + /// Generates a random value. + /// + /// + /// A randomly generated value within the range of to . + /// + public static sbyte SByte() + { + return (sbyte)Random.Next(sbyte.MinValue, sbyte.MaxValue); + } + + /// + /// Generates a random value. + /// + /// + /// A randomly generated value within the range of to . + /// + public static byte Byte() + { + return (byte)Random.Next(byte.MinValue, byte.MaxValue); + } + + /// + /// Generates a random value. + /// + /// + /// A randomly generated value within the range of to . + /// + public static short Short() + { + return (short)Random.Next(short.MinValue, short.MaxValue); + } + + /// + /// Generates a random value. + /// + /// + /// A randomly generated value within the range of to . + /// + public static ushort UShort() + { + return (ushort)Random.Next(ushort.MinValue, ushort.MaxValue); + } + + /// + /// Generates a random value. + /// + /// + /// In contrast to , this method can return the full range of values, including negative values. + /// + /// + /// A randomly generated value within the range of to . + /// + public static int Int() + { + return Random.Next(int.MinValue, int.MaxValue); + } + + /// + /// Generates a random value. + /// + /// + /// A randomly generated value within the range of to . + /// + public static uint UInt() + { + return (uint)Random.Int(); + } + + /// + /// Generates a random value. + /// + /// + /// A randomly generated value within the range of to . + /// + public static long Long() + { +#if NET || NETSTANDARD2_1_OR_GREATER + Span buf = stackalloc byte[8]; + Random.NextBytes(buf); + return BitConverter.ToInt64(buf); +#else + var buf = new byte[8]; + Random.NextBytes(buf); + return BitConverter.ToInt64(buf, 0); +#endif + } + + /// + /// Generates a random value. + /// + /// + /// A randomly generated value within the range of to . + /// + public static ulong ULong() + { + return (ulong)Random.Long(); + } + + /// + /// Returns a random value from the enumeration type . + /// + /// + /// The enumeration type from which a random value will be selected. Must be a struct and an . + /// + /// + /// A randomly selected value from the enumeration type . + /// + /// + /// Thrown if is not an enumeration type. + /// + public static T Enum() where T : struct, Enum + { + var values = +#if NET5_0_OR_GREATER + System.Enum.GetValues(); +#else + (T[])System.Enum.GetValues(typeof(T)); +#endif + return values[Random.Next(values.Length)]; + } + + + // From: https://stackoverflow.com/questions/648196/random-row-from-linq-to-sql/648240#648240 + /// + /// Selects a random item from the provided sequence of items. + /// + /// The type of the elements in the sequence. + /// The sequence of items to select from. + /// A randomly selected item from the sequence. + /// Thrown if the sequence is empty. + public static T Item(IEnumerable items) + { + T current = default!; + var count = 0; + foreach (var element in items) + { + count++; + if (Random.Next(count) == 0) + current = element; + } + return count == 0 + ? throw new InvalidOperationException("Sequence was empty") + : current; + } + + /// + /// Generates a random string of the specified length using a mix of letters (any case), numbers and special characters + /// + /// The desired length of the generated string. Must be a non-negative value. + /// A randomly generated string of the specified length. Returns an empty string if is 0. + /// Thrown when is less than 0. + public static string String(int length) + { + return Random.String(length, AllowedChars); + } + + /// + /// Generates a random string of the specified length using the specified pool of characters. + /// + /// The desired length of the generated string. Must be a non-negative value. + /// The pool of characters to pick random characters from. + /// A randomly generated string of the specified length. Returns an empty string if is 0. + /// Thrown when is less than 0. + /// Thrown when is empty or . + public static unsafe string String(int length, ReadOnlySpan charPool) + { + if (charPool == ReadOnlySpan.Empty || charPool.IsEmpty) + throw new ArgumentException("charPool must not be null or empty", nameof(charPool)); + switch (length) + { + case < 0: + throw new ArgumentOutOfRangeException(nameof(length)); + case 0: + return string.Empty; + } + + var buffer = length <= 256 + ? stackalloc char[length] + : new char[length]; + + var random = Random; + for (var i = 0; i < buffer.Length; i++) + { + var index = random.Next(charPool.Length); + buffer[i] = charPool[index]; + } + +#if NET + return new string(buffer); +#else + fixed (char* t = buffer) + return new string(t, 0, length); +#endif + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/Extensions/StringExtensions.cs b/src/CommonUtilities.Testing/Extensions/StringExtensions.cs new file mode 100644 index 0000000..dd1319c --- /dev/null +++ b/src/CommonUtilities.Testing/Extensions/StringExtensions.cs @@ -0,0 +1,58 @@ +using System; + +namespace AnakinRaW.CommonUtilities.Extensions; + +/// +/// Provides extension methods for string manipulation and testing. +/// +public static class StringExtensions +{ + private static readonly Random Random = new(); + + extension(string) + { + /// + /// Randomly shuffles the casing of the characters in the specified string. + /// + /// The input string whose character casing will be shuffled. + /// A new string with randomly shuffled character casing. + /// Thrown if is null. + public static unsafe string ShuffleCasing(string input) + { + if (input is null) + throw new ArgumentNullException(nameof(input)); + + if (input.Length == 0) + return string.Empty; + + var buffer = input.Length <= 256 + ? stackalloc char[input.Length] + : new char[input.Length]; + + input.AsSpan().CopyTo(buffer); + + var rnd = Random; + + for (var i = 0; i < buffer.Length; i++) + { + var c = buffer[i]; + if (!char.IsLetter(c)) + continue; + + if (rnd.Next(2) != 0) + continue; + + buffer[i] = char.IsUpper(c) + ? char.ToLower(c) + : char.ToUpper(c); + } + +#if NET + return new string(buffer); +#else + fixed (char* t = buffer) + return new string(t, 0, buffer.Length); +#endif + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/TestBaseWithFileSystem.cs b/src/CommonUtilities.Testing/TestBaseWithFileSystem.cs new file mode 100644 index 0000000..192e26b --- /dev/null +++ b/src/CommonUtilities.Testing/TestBaseWithFileSystem.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO.Abstractions; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Testably.Abstractions.Testing; + +namespace AnakinRaW.CommonUtilities.Testing; + +/// +/// A test base that automatically registers an as service. +/// +public abstract class TestBaseWithFileSystem : TestBaseWithServiceProvider +{ + /// + /// Gets the file system abstraction used for testing purposes. + /// This property provides access to an instance, which is lazily initialized + /// and can be overridden by derived classes to customize the file system behavior. + /// + /// + /// The file system is initialized using the method. If the initialization + /// fails or returns null, an is thrown. + /// + [field: MaybeNull, AllowNull] + protected IFileSystem FileSystem => LazyInitializer.EnsureInitialized(ref field, CreateFileSystem) + ?? throw new InvalidOperationException("Creation of file system must not return null."); + + /// + /// Initializes a new instance of the class and configures the service provider. + /// + /// + /// This constructor creates a new service collection, invokes the method to allow + /// derived classes to register services, and then builds the service provider. Derived classes should override + /// SetupServices to customize service registration. + /// + protected TestBaseWithFileSystem() + { + } + + /// + /// Creates and returns a new instance of the file system abstraction for testing purposes. + /// + /// + /// This method is invoked to initialize the property. By default, it returns + /// a instance, but derived classes can override this method to provide + /// a custom implementation of . + /// + /// + /// An instance of representing the file system abstraction to be used in tests. + /// + protected virtual IFileSystem CreateFileSystem() + { + return new MockFileSystem(); + } + + + /// + protected override void SetupServices(IServiceCollection serviceCollection) + { + base.SetupServices(serviceCollection); + serviceCollection.AddSingleton(FileSystem); + } +} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/TestBaseWithServiceProvider.cs b/src/CommonUtilities.Testing/TestBaseWithServiceProvider.cs new file mode 100644 index 0000000..232d931 --- /dev/null +++ b/src/CommonUtilities.Testing/TestBaseWithServiceProvider.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace AnakinRaW.CommonUtilities.Testing; + +/// +/// Provides a base class for test fixtures that provides and for dependency injection. +/// +/// Derive from this class to set up and access services using dependency injection in test scenarios. +/// Override to register custom services required for your +/// tests. +public abstract class TestBaseWithServiceProvider +{ + /// + /// Provides access to the application's service provider for resolving dependencies within derived classes. + /// + protected readonly IServiceProvider ServiceProvider; + + /// + /// Initializes a new instance of the class and configures the service provider. + /// + /// + /// This constructor creates a new service collection, invokes the method to allow + /// derived classes to register services, and then builds the service provider. Derived classes should override + /// SetupServices to customize service registration. + /// + protected TestBaseWithServiceProvider() + { + var sc = new ServiceCollection(); + // ReSharper disable once VirtualMemberCallInConstructor + SetupServices(sc); + ServiceProvider = sc.BuildServiceProvider(); + } + + /// + /// Configures test services by adding them to the specified . + /// + /// The to which services will be added. + protected virtual void SetupServices(IServiceCollection serviceCollection) + { + } +} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/TestingHelpers.cs b/src/CommonUtilities.Testing/TestingHelpers.cs new file mode 100644 index 0000000..2ccf751 --- /dev/null +++ b/src/CommonUtilities.Testing/TestingHelpers.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; + +namespace AET.Testing; + +/// +/// Provides common helper methods useful creating test code. +/// +public class TestingHelpers +{ + /// + /// Retrieves an embedded resource stream from the specified assembly and path. + /// + /// + /// Embedded resources are expected to be located in the "Resources" folder of the assembly. + /// + /// A from the assembly containing the embedded resource. + /// The relative path of the embedded resource within the assembly. + /// A representing the embedded resource. + /// Thrown when the specified embedded resource cannot be found. + public static Stream GetEmbeddedResource(Type type, string path) + { + var assembly = type.Assembly; + var resourcePath = $"{assembly.GetName().Name}.Resources.{path}"; + return assembly.GetManifestResourceStream(resourcePath) ?? + throw new IOException($"Could not find embedded resource: '{resourcePath}'"); + } + + /// + /// Retrieves an embedded resource as a byte array from the specified assembly and path. + /// + /// + /// Embedded resources are expected to be located in the "Resources" folder of the assembly. + /// + /// A from the assembly containing the embedded resource. + /// The relative path of the embedded resource within the assembly. + /// A byte array containing the content of the embedded resource. + /// Thrown when the specified embedded resource cannot be found. + public static byte[] GetEmbeddedResourceAsByteArray(Type type, string path) + { + using var stream = GetEmbeddedResource(type, path); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + ms.Position = 0; + return ms.ToArray(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/AssertExtensions.cs b/src/CommonUtilities.TestingUtilities/AssertExtensions.cs deleted file mode 100644 index 3ac28e6..0000000 --- a/src/CommonUtilities.TestingUtilities/AssertExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -namespace AnakinRaW.CommonUtilities.Testing; - -public static class AssertExtensions -{ - private static bool IsNetFramework => RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"); - - public static void Throws_IgnoreTargetInvocationException(Func action) where T : Exception - { - Throws_IgnoreTargetInvocationException(typeof(T), action); - } - - public static void Throws_IgnoreTargetInvocationException(Type expectedException, Func action) - { - if (expectedException.IsAssignableFrom(typeof(Exception))) - throw new ArgumentException("Type argument must be assignable from System.Exception", nameof(expectedException)); - try - { - action(); - } - catch (TargetInvocationException e) - { - if (e.InnerException?.GetType() != expectedException) - Assert.Fail($"Expected exception of type {expectedException.Name} but got {e.InnerException?.GetType().Name}"); - return; - } - catch (Exception e) - { - if (e.GetType() == expectedException) - return; - Assert.Fail($"Expected exception of type {expectedException.Name} but got {e.GetType().Name}"); - } - Assert.Fail($"Excepted exception of type {expectedException.Name} but non was thrown."); - } - - public static T Throws(string? expectedParamName, Action action) where T : ArgumentException - { - T exception = Assert.Throws(action); - Assert.Equal(expectedParamName, exception.ParamName); - return exception; - } - - public static T Throws(string netCoreParamName, string? netFxParamName, Action action) - where T : ArgumentException - { - var exception = Assert.Throws(action); - - if (netFxParamName == null && IsNetFramework) - { - // Param name varies between .NET Framework versions -- skip checking it - return exception; - } - - var expectedParamName = IsNetFramework ? netFxParamName : netCoreParamName; - - Assert.Equal(expectedParamName, exception.ParamName); - return exception; - } -} \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/CollectionAsserts.cs b/src/CommonUtilities.TestingUtilities/CollectionAsserts.cs deleted file mode 100644 index 5a426cc..0000000 --- a/src/CommonUtilities.TestingUtilities/CollectionAsserts.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace AnakinRaW.CommonUtilities.Testing; - -internal class CollectionAsserts -{ - public static void EqualUnordered(ICollection expected, ICollection actual) - { - Assert.Equal(expected == null, actual == null); - if (expected == null) - { - return; - } - - // Lookups are an aggregated collections (enumerable contents), but ordered. - var e = expected.Cast().ToLookup(key => key); - var a = actual!.Cast().ToLookup(key => key); - - // Dictionaries can't handle null keys, which is a possibility - Assert.Equal(e.Where(kv => kv.Key != null).ToDictionary(g => g.Key, g => g.Count()), - a.Where(kv => kv.Key != null).ToDictionary(g => g.Key, g => g.Count())); - - // Get count of null keys. Returns an empty sequence (and thus a 0 count) if no null key - Assert.Equal(e[null!].Count(), a[null!].Count()); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/CommonTestBase.cs b/src/CommonUtilities.TestingUtilities/CommonTestBase.cs deleted file mode 100644 index 3bbf416..0000000 --- a/src/CommonUtilities.TestingUtilities/CommonTestBase.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.IO.Abstractions; -using Microsoft.Extensions.DependencyInjection; -using Testably.Abstractions.Testing; - -namespace AnakinRaW.CommonUtilities.Testing; - -public abstract class CommonTestBase -{ - protected readonly IServiceProvider ServiceProvider; - - protected readonly MockFileSystem FileSystem = new (); - - protected CommonTestBase() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(FileSystem); - - // ReSharper disable once VirtualMemberCallInConstructor - SetupServices(serviceCollection); - ServiceProvider = serviceCollection.BuildServiceProvider(); - } - - protected virtual void SetupServices(IServiceCollection serviceCollection) - { - } -} \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/ConditionalFactAttribute.cs b/src/CommonUtilities.TestingUtilities/ConditionalFactAttribute.cs deleted file mode 100644 index d8d2908..0000000 --- a/src/CommonUtilities.TestingUtilities/ConditionalFactAttribute.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Runtime.InteropServices; - -namespace AnakinRaW.CommonUtilities.Testing; - -public sealed class PlatformSpecificFactAttribute : FactAttribute -{ - public PlatformSpecificFactAttribute(params TestPlatformIdentifier[] platformIds) - { - var platforms = platformIds.Select(targetPlatform => OSPlatform.Create(Enum.GetName(typeof(TestPlatformIdentifier), targetPlatform)!.ToUpper())); - var platformMatches = platforms.Any(RuntimeInformation.IsOSPlatform); - - if (!platformMatches) - Skip = "Test execution is not supported on the current platform"; - } -} - -public sealed class PlatformSpecificTheoryAttribute : TheoryAttribute -{ - public PlatformSpecificTheoryAttribute(params TestPlatformIdentifier[] platformIds) - { - var platforms = platformIds.Select(targetPlatform => OSPlatform.Create(Enum.GetName(typeof(TestPlatformIdentifier), targetPlatform)!.ToUpper())); - var platformMatches = platforms.Any(RuntimeInformation.IsOSPlatform); - - if (!platformMatches) - Skip = "Test execution is not supported on the current platform"; - } -} - - -[Flags] -public enum TestPlatformIdentifier -{ - Windows = 1, - Linux = 2, -} \ No newline at end of file diff --git a/src/CommonUtilities.TestingUtilities/EqualityComparerConstantHashCode.cs b/src/CommonUtilities.TestingUtilities/EqualityComparerConstantHashCode.cs deleted file mode 100644 index bde2db6..0000000 --- a/src/CommonUtilities.TestingUtilities/EqualityComparerConstantHashCode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AnakinRaW.CommonUtilities.Testing; - -public sealed class EqualityComparerConstantHashCode(IEqualityComparer comparer) : IEqualityComparer -{ - public bool Equals(T x, T y) => comparer.Equals(x, y); - - public int GetHashCode(T obj) => 42; -} \ No newline at end of file diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index 36908c0..bc19df5 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -1,15 +1,17 @@ - + + CommonUtilities + AnakinRaW.CommonUtilities Provides common classes and utilities for personal use. + true netstandard2.0;netstandard2.1;net8.0 AnakinRaW.CommonUtilities AnakinRaW.CommonUtilities - enable - true + en diff --git a/src/CommonUtilities/src/DisposableObject.cs b/src/CommonUtilities/src/DisposableObject.cs index 70b46ab..b529b99 100644 --- a/src/CommonUtilities/src/DisposableObject.cs +++ b/src/CommonUtilities/src/DisposableObject.cs @@ -29,7 +29,11 @@ public event EventHandler Disposing /// public bool IsDisposed { get; private set; } - /// + /// + /// Finalizer for the class. + /// Ensures that unmanaged resources are released when the object is garbage collected, + /// if they have not already been released by calling . + /// ~DisposableObject() { Dispose(false); diff --git a/src/CommonUtilities/src/Extensions/EncodingExtensions.cs b/src/CommonUtilities/src/Extensions/EncodingExtensions.cs index 204917e..c47ce12 100644 --- a/src/CommonUtilities/src/Extensions/EncodingExtensions.cs +++ b/src/CommonUtilities/src/Extensions/EncodingExtensions.cs @@ -90,7 +90,7 @@ public static int EncodeString(this Encoding encoding, ReadOnlySpan source if (encoding == null) throw new ArgumentNullException(nameof(encoding)); var numMaxBytes = encoding.GetMaxByteCount(source.Length); - return EncodeString(encoding, source, destination, numMaxBytes); + return encoding.EncodeString(source, destination, numMaxBytes); } /// diff --git a/src/CommonUtilities/test/AwaitExtensionsTests.cs b/src/CommonUtilities/test/AwaitExtensionsTests.cs index c7ba847..19bf093 100644 --- a/src/CommonUtilities/test/AwaitExtensionsTests.cs +++ b/src/CommonUtilities/test/AwaitExtensionsTests.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Attributes; using Xunit; namespace AnakinRaW.CommonUtilities.Test; @@ -14,7 +14,7 @@ public class AwaitExtensionsTests [Fact] public async Task WaitForExitAsync_NullArgument() { - await Assert.ThrowsAsync(() => AwaitExtensions.WaitForExitAsync(null!)); + await Assert.ThrowsAsync(() => AwaitExtensions.WaitForExitAsync(null!, TestContext.Current.CancellationToken)); } [PlatformSpecificFact(TestPlatformIdentifier.Windows)] @@ -26,7 +26,7 @@ public async Task WaitForExitAsync_ExitCode_Windows() CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, })!; - await AwaitExtensions.WaitForExitAsync(p); + await p.WaitForExitAsync(); Assert.Equal(55, p.ExitCode); } @@ -40,7 +40,7 @@ public void WaitForExitAsync_AlreadyExited_Windows() WindowStyle = ProcessWindowStyle.Hidden, })!; p.WaitForExit(); - var t = AwaitExtensions.WaitForExitAsync(p); + var t = p.WaitForExitAsync(); Assert.True(t.IsCompleted); Assert.Equal(55, p.ExitCode); } @@ -52,7 +52,7 @@ public async Task WaitForExitAsync_UnstartedProcess() var process = new System.Diagnostics.Process(); process.StartInfo.FileName = processName; process.StartInfo.CreateNoWindow = true; - await Assert.ThrowsAsync(() => process.WaitForExitAsync()); + await Assert.ThrowsAsync(() => process.WaitForExitAsync(TestContext.Current.CancellationToken)); } [Fact] @@ -71,7 +71,7 @@ public async Task WaitForExitAsync_DoesNotCompleteTillKilled() RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? -1 : 128 + 9; // https://stackoverflow.com/a/1041309 try { - var t = AwaitExtensions.WaitForExitAsync(p); + var t = p.WaitForExitAsync(TestContext.Current.CancellationToken); Assert.False(t.IsCompleted); p.Kill(); await t; @@ -107,7 +107,7 @@ public async Task WaitForExitAsync_Canceled() try { var cts = new CancellationTokenSource(); - var t = AwaitExtensions.WaitForExitAsync(p, cts.Token); + var t = p.WaitForExitAsync(cts.Token); Assert.False(t.IsCompleted); cts.Cancel(); await Assert.ThrowsAsync(() => t); diff --git a/src/CommonUtilities/test/Collections/DebugViewTest.cs b/src/CommonUtilities/test/Collections/DebugViewTest.cs index cdd98e3..5cf3b4e 100644 --- a/src/CommonUtilities/test/Collections/DebugViewTest.cs +++ b/src/CommonUtilities/test/Collections/DebugViewTest.cs @@ -188,12 +188,12 @@ internal static DebuggerDisplayResult ValidateDebugViewValueListDictionaryItem na.MemberName == argumentName); + var namedAttribute = debuggerDisplayAttributeData.NamedArguments!.FirstOrDefault(na => na.MemberName == argumentName); if (namedAttribute != default) { var value = (string?)namedAttribute.TypedValue.Value; if (!string.IsNullOrEmpty(value)) - return EvaluateDisplayString(value, obj); + return EvaluateDisplayString(value!, obj); } return ""; } diff --git a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs index 4d0712a..978d430 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs @@ -46,6 +46,7 @@ public void Struct_Default() [Fact] public void Constructor_Empty() { + // ReSharper disable once CollectionNeverUpdated.Local var list = new FrugalList(); Assert.Equal(0, list.Count); Assert.False(list.IsReadOnly); @@ -55,6 +56,7 @@ public void Constructor_Empty() public void Constructor_Single() { var t = CreateT(0); + // ReSharper disable once CollectionNeverUpdated.Local var list = new FrugalList(t); Assert.Equal(1, list.Count); Assert.Equal(t, list[0]); @@ -84,14 +86,13 @@ public void Constructor_OtherFrugalList_Creates_Copy(int count) public void Constructor_IEnumerable(int _, int enumerableLength, int __, int numberOfDuplicateElements) #pragma warning restore xUnit1026 { - var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); + var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements).ToList(); var list = new FrugalList(enumerable); - var expected = enumerable.ToList(); Assert.Equal(enumerableLength, list.Count); //"Number of items in list do not match the number of items given." for (var i = 0; i < enumerableLength; i++) - Assert.Equal(expected[i], list[i]); //"Expected object in item array to be the same as in the list" + Assert.Equal(enumerable[i], list[i]); //"Expected object in item array to be the same as in the list" Assert.False(list.IsReadOnly); //"List should not be readonly" } @@ -104,7 +105,7 @@ public void Constructor_IEnumerable_Creates_Copy(int _, int enumerableLength, in { foreach (var modifyEnumerable in GetModifyEnumerables(ModifyEnumeratorThrows)) { - var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); + var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements).ToList(); var list = new FrugalList(enumerable); if (modifyEnumerable(enumerable)) @@ -126,6 +127,7 @@ public void Constructor_NullIEnumerable_ThrowsArgumentNullException() [MemberData(nameof(ValidCollectionSizes))] public void Boxing_ReflectsAllChanges(int count) { + // ReSharper disable PossibleMultipleEnumeration foreach (var modifyEnumerable in GetModifyEnumerables(ModifyEnumeratorThrows)) { var source = GenericIEnumerableFactory(count); @@ -134,6 +136,7 @@ public void Boxing_ReflectsAllChanges(int count) if (modifyEnumerable(source)) Assert.Equal(source.ToList(), copy.ToList()); } + // ReSharper restore PossibleMultipleEnumeration } [Theory] diff --git a/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs index cbf8235..6ce4164 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs @@ -257,7 +257,7 @@ public void IList_Generic_IndexOf_EachValueNoDuplicates(int count) [MemberData(nameof(ValidCollectionSizes))] public void ToList(int count) { - var enumerable = CreateEnumerable(null, count, 0, 0); + var enumerable = CreateEnumerable(null, count, 0, 0).ToList(); var list = new FrugalList(enumerable); Assert.Equal(enumerable.ToList(), list.ToList()); } @@ -320,7 +320,7 @@ public void ToArray(int count) public void GetEnumerator(int _, int enumerableLength, int __, int numberOfDuplicateElements) #pragma warning restore xUnit1026 { - var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); + var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements).ToList(); var list = new FrugalList(enumerable); var actualList = new List(); diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs index b05b1b1..fbe90ec 100644 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs @@ -274,7 +274,7 @@ public void KeyCount_Validity(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_ContainsKey_ValidKeyNotContainedInDictionary(int count) + public void ContainsKey_ValidKeyNotContainedInDictionary(int count) { var dictionary = IReadOnlyValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -283,7 +283,7 @@ public void IDictionary_Generic_ContainsKey_ValidKeyNotContainedInDictionary(int [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_ContainsKey_ValidKeyContainedInDictionary(int count) + public void ContainsKey_ValidKeyContainedInDictionary(int count) { var dictionary = IReadOnlyValueListDictionaryFactory(count); if (count > 0) @@ -302,7 +302,7 @@ public void IDictionary_Generic_ContainsKey_ValidKeyContainedInDictionary(int co [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_ContainsKey_DefaultKeyNotContainedInDictionary(int count) + public void ContainsKey_DefaultKeyNotContainedInDictionary(int count) { var dictionary = IReadOnlyValueListDictionaryFactory(count); if (DefaultValueAllowed) @@ -325,7 +325,7 @@ public void IDictionary_Generic_ContainsKey_DefaultKeyNotContainedInDictionary(i [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_ContainsKey_DefaultKeyContainedInDictionary(int count) + public void ContainsKey_DefaultKeyContainedInDictionary(int count) { if (DefaultValueAllowed && !IsReadOnly) { diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs index fbbd86f..790b6c7 100644 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs @@ -110,7 +110,7 @@ public void Values_ModifyingTheDictionaryUpdatesTheCollection(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Add_DefaultKey_DefaultValue(int count) + public void Add_DefaultKey_DefaultValue(int count) { var dictionary = IValueListDictionaryFactory(count); var valueCoutBeforeAdd = dictionary.Count; @@ -132,7 +132,7 @@ public void IDictionary_Generic_Add_DefaultKey_DefaultValue(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Add_DefaultKey_NonDefaultValue(int count) + public void Add_DefaultKey_NonDefaultValue(int count) { var dictionary = IValueListDictionaryFactory(count); var valueCoutBeforeAdd = dictionary.Count; @@ -154,7 +154,7 @@ public void IDictionary_Generic_Add_DefaultKey_NonDefaultValue(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Add_NonDefaultKey_DefaultValue(int count) + public void Add_NonDefaultKey_DefaultValue(int count) { var dictionary = IValueListDictionaryFactory(count); var valueCoutBeforeAdd = dictionary.Count; @@ -169,7 +169,7 @@ public void IDictionary_Generic_Add_NonDefaultKey_DefaultValue(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Add_NonDefaultKey_NonDefaultValue(int count) + public void Add_NonDefaultKey_NonDefaultValue(int count) { var dictionary = IValueListDictionaryFactory(count); var valueCoutBeforeAdd = dictionary.Count; @@ -184,7 +184,7 @@ public void IDictionary_Generic_Add_NonDefaultKey_NonDefaultValue(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Add_DuplicateValue(int count) + public void Add_DuplicateValue(int count) { var dictionary = IValueListDictionaryFactory(count); var seed = 321; @@ -198,7 +198,7 @@ public void IDictionary_Generic_Add_DuplicateValue(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Add_DuplicateKey_AddsToList(int count) + public void Add_DuplicateKey_AddsToList(int count) { var dictionary = IValueListDictionaryFactory(count); var duplicateKey = GetNewKey(dictionary); @@ -215,9 +215,9 @@ public void IDictionary_Generic_Add_DuplicateKey_AddsToList(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Add_DistinctValuesWithHashCollisions(int count) + public void Add_DistinctValuesWithHashCollisions(int count) { - var dictionary = IValueListDictionaryFactory(new EqualityComparerConstantHashCode(EqualityComparer.Default)); + var dictionary = IValueListDictionaryFactory(new ConstantHashCodeEqualityComparer(EqualityComparer.Default)); AddToCollection(dictionary, count); Assert.Equal(count, dictionary.KeyCount); } @@ -228,7 +228,7 @@ public void IDictionary_Generic_Add_DistinctValuesWithHashCollisions(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Remove_EveryKey(int count) + public void Remove_EveryKey(int count) { var dictionary = IValueListDictionaryFactory(count); Assert.All(dictionary.Keys.ToList(), key => @@ -240,7 +240,7 @@ public void IDictionary_Generic_Remove_EveryKey(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Remove_ValidKeyNotContainedInDictionary(int count) + public void Remove_ValidKeyNotContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -250,7 +250,7 @@ public void IDictionary_Generic_Remove_ValidKeyNotContainedInDictionary(int coun [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Remove_ValidKeyContainedInDictionary(int count) + public void Remove_ValidKeyContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -261,7 +261,7 @@ public void IDictionary_Generic_Remove_ValidKeyContainedInDictionary(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Remove_DefaultKeyNotContainedInDictionary(int count) + public void Remove_DefaultKeyNotContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); if (DefaultValueAllowed) @@ -279,7 +279,7 @@ public void IDictionary_Generic_Remove_DefaultKeyNotContainedInDictionary(int co [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_Remove_DefaultKeyContainedInDictionary(int count) + public void Remove_DefaultKeyContainedInDictionary(int count) { if (DefaultValueAllowed) { @@ -296,7 +296,7 @@ public void IDictionary_Generic_Remove_DefaultKeyContainedInDictionary(int count [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_RemoveKeyValue_Everything(int count) + public void RemoveKeyValue_Everything(int count) { var dictionary = IValueListDictionaryFactory(count); Assert.All(dictionary.Keys.ToList(), key => @@ -309,7 +309,7 @@ public void IDictionary_Generic_RemoveKeyValue_Everything(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_RemoveKeyValue_ValidKeyNotContainedInDictionary(int count) + public void RemoveKeyValue_ValidKeyNotContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -319,7 +319,7 @@ public void IDictionary_Generic_RemoveKeyValue_ValidKeyNotContainedInDictionary( [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary(int count) + public void RemoveKeyValue_ValidKeyContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -331,7 +331,7 @@ public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary(int [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary_ValueNotContained(int count) + public void RemoveKeyValue_ValidKeyContainedInDictionary_ValueNotContained(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -349,7 +349,7 @@ public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary_Val [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary_DuplicateValues(int count) + public void RemoveKeyValue_ValidKeyContainedInDictionary_DuplicateValues(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -366,7 +366,7 @@ public void IDictionary_Generic_RemoveKeyValue_ValidKeyContainedInDictionary_Dup [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_RemoveKeyValue_DefaultKeyNotContainedInDictionary(int count) + public void RemoveKeyValue_DefaultKeyNotContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); if (DefaultValueAllowed) @@ -384,7 +384,7 @@ public void IDictionary_Generic_RemoveKeyValue_DefaultKeyNotContainedInDictionar [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void IDictionary_Generic_RemoveKeyValue_DefaultKeyContainedInDictionary(int count) + public void RemoveKeyValue_DefaultKeyContainedInDictionary(int count) { if (DefaultValueAllowed) { diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs index 69aaa70..8a8cf7a 100644 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs @@ -1,5 +1,5 @@ using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Extensions; using System; using System.Collections.Generic; using Xunit; diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index a481d78..fb8a021 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -5,32 +5,23 @@ $(TargetFrameworks);net481 false true + Exe AnakinRaW.CommonUtilities.Test AnakinRaW.CommonUtilities.Test true - enable - - - - - - + - - + + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -46,7 +37,7 @@ - + diff --git a/src/CommonUtilities/test/Extensions/EncodingExtensionsTest.cs b/src/CommonUtilities/test/Extensions/EncodingExtensionsTest.cs index dc35d93..edacfd1 100644 --- a/src/CommonUtilities/test/Extensions/EncodingExtensionsTest.cs +++ b/src/CommonUtilities/test/Extensions/EncodingExtensionsTest.cs @@ -145,8 +145,10 @@ public void EncodeString_NullArgs_Throws() ForEachEncoding(e => { + // ReSharper disable RedundantCast Assert.Throws(() => e.EncodeString((string)null!)); Assert.Throws(() => e.EncodeString((string)null!, 0)); + // ReSharper restore RedundantCast }); } diff --git a/src/CommonUtilities/test/Hashing/HashingServiceTest.cs b/src/CommonUtilities/test/Hashing/HashingServiceTest.cs index 8082c67..864106d 100644 --- a/src/CommonUtilities/test/Hashing/HashingServiceTest.cs +++ b/src/CommonUtilities/test/Hashing/HashingServiceTest.cs @@ -81,10 +81,10 @@ public async Task GetHashAsync_ProviderNotFound_ThrowsHashProviderNotFoundExcept var someDestination = new byte[1]; var someStream = new MemoryStream(someSource); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), notExistingProvider)); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), someDestination.AsMemory(), notExistingProvider)); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, notExistingProvider)); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, someDestination.AsMemory(), notExistingProvider)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), notExistingProvider, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), someDestination.AsMemory(), notExistingProvider, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, notExistingProvider, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, someDestination.AsMemory(), notExistingProvider, TestContext.Current.CancellationToken)); } @@ -115,8 +115,8 @@ public async Task GetHashAsync_AlwaysOneProvider_DestinationTooShort_ThrowsIndex var someDestination = Array.Empty(); var someStream = new MemoryStream(someSource); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), someDestination.AsMemory(), provider)); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, someDestination.AsMemory(), provider)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), someDestination.AsMemory(), provider, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, someDestination.AsMemory(), provider, TestContext.Current.CancellationToken)); } [Theory] @@ -181,10 +181,10 @@ public async Task GetHashAsync_WrongOutputSizeProvider_HashWrongSize_ThrowsInval var someDestination = new byte[2]; var someStream = new MemoryStream(someSource); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), notExistingProvider)); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), someDestination.AsMemory(), notExistingProvider)); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, someDestination.AsMemory(), notExistingProvider)); - await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, notExistingProvider)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), notExistingProvider, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), someDestination.AsMemory(), notExistingProvider, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, someDestination.AsMemory(), notExistingProvider, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await _hashingService.GetHashAsync(someStream, notExistingProvider, TestContext.Current.CancellationToken)); } @@ -240,14 +240,14 @@ public async Task GetHashAsync_AlwaysOneProvider() var expectedHashExact = new byte[] { 1 }; var expectedHashJoint = new byte[] { 1, 0 }; - Assert.Equal(expectedHashExact, await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), provider)); + Assert.Equal(expectedHashExact, await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), provider, TestContext.Current.CancellationToken)); - Assert.Equal(1, await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), destination.AsMemory(), provider)); + Assert.Equal(1, await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), destination.AsMemory(), provider, TestContext.Current.CancellationToken)); Assert.Equal(expectedHashJoint, destination); - Assert.Equal(expectedHashExact, await _hashingService.GetHashAsync(someStream, provider)); + Assert.Equal(expectedHashExact, await _hashingService.GetHashAsync(someStream, provider, TestContext.Current.CancellationToken)); - Assert.Equal(1, await _hashingService.GetHashAsync(someStream, destination.AsMemory(), provider)); + Assert.Equal(1, await _hashingService.GetHashAsync(someStream, destination.AsMemory(), provider, TestContext.Current.CancellationToken)); Assert.Equal(expectedHashJoint, destination); } @@ -324,16 +324,16 @@ public async Task GetHashAsync_DefaultProviders(HashTypeKey hashType, string inp var someStream = new MemoryStream(someSource); var destination = new byte[expectedSize]; - Assert.Equal(expectedHash, await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), hashType)); + Assert.Equal(expectedHash, await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), hashType, TestContext.Current.CancellationToken)); - Assert.Equal(expectedSize, await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), destination.AsMemory(), hashType)); + Assert.Equal(expectedSize, await _hashingService.GetHashAsync(_fileSystem.FileInfo.New("test.txt"), destination.AsMemory(), hashType, TestContext.Current.CancellationToken)); Assert.Equal(expectedHash, destination); - Assert.Equal(expectedHash, await _hashingService.GetHashAsync(someStream, hashType)); + Assert.Equal(expectedHash, await _hashingService.GetHashAsync(someStream, hashType, TestContext.Current.CancellationToken)); someStream.Seek(0, SeekOrigin.Begin); - Assert.Equal(expectedSize, await _hashingService.GetHashAsync(someStream, destination.AsMemory(), hashType)); + Assert.Equal(expectedSize, await _hashingService.GetHashAsync(someStream, destination.AsMemory(), hashType, TestContext.Current.CancellationToken)); Assert.Equal(expectedHash, destination); } diff --git a/src/CommonUtilities/test/ThrowHelperTest.cs b/src/CommonUtilities/test/ThrowHelperTest.cs index 177c521..867a6ef 100644 --- a/src/CommonUtilities/test/ThrowHelperTest.cs +++ b/src/CommonUtilities/test/ThrowHelperTest.cs @@ -1,7 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Extensions; using Xunit; namespace AnakinRaW.CommonUtilities.Test; @@ -22,29 +22,31 @@ public static void ThrowIfNullOrEmpty_ThrowsForInvalidInput() ThrowHelper.ThrowIfNullOrEmpty("abc", "something"); } + // ReSharper disable AccessToModifiedClosure [Fact] public static void ThrowIfNullOrEmpty_UsesArgumentExpression_ParameterNameMatches() { - string someString = null; + string? someString = null; AssertExtensions.Throws(nameof(someString), () => ThrowHelper.ThrowIfNullOrEmpty(someString)); someString = ""; - AssertExtensions.Throws(nameof(someString), () => ThrowHelper.ThrowIfNullOrEmpty(someString)); + AssertExtensions.Throws(nameof(someString), () => ThrowHelper.ThrowIfNullOrEmpty(someString!)); someString = "abc"; ThrowHelper.ThrowIfNullOrEmpty(someString); } + // ReSharper restore AccessToModifiedClosure [Fact] public static void ThrowIfCollectionNullOrEmpty_ThrowsForInvalidInput() { AssertExtensions.Throws(null, () => ThrowHelper.ThrowIfCollectionNullOrEmpty(null, null)); - AssertExtensions.Throws(null, () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)null, null)); - AssertExtensions.Throws(null, () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)null, null)); + AssertExtensions.Throws(null, () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)null!, null)); + AssertExtensions.Throws(null, () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)null!, null)); AssertExtensions.Throws("something", () => ThrowHelper.ThrowIfCollectionNullOrEmpty(null, "something")); - AssertExtensions.Throws("something", () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)null, "something")); - AssertExtensions.Throws("something", () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)null, "something")); + AssertExtensions.Throws("something", () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)null!, "something")); + AssertExtensions.Throws("something", () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)null!, "something")); AssertExtensions.Throws(null, () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)new List(), null)); AssertExtensions.Throws(null, () => ThrowHelper.ThrowIfCollectionNullOrEmpty((IList)new List(), null)); From ae1c7c954b85bdebd11d6f3025a115ca71321bff Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 28 Dec 2025 14:58:21 +0100 Subject: [PATCH 30/43] fix merge --- .../CommonUtilities.DownloadManager.csproj | 3 --- .../test/CommonUtilities.Test.csproj | 24 +++++++++---------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index ced92d7..e80e4fc 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -36,9 +36,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index 4f3ef8e..fa8d08c 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -15,22 +15,20 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -39,4 +37,4 @@ - + \ No newline at end of file From 2e9b2ff99cd24239a5b9b18aaf2af06ab93ea41e Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 28 Dec 2025 15:05:25 +0100 Subject: [PATCH 31/43] fix build for real --- .../test/Validation/HashDownloadValidatorTest.cs | 10 +--------- .../Collections/FrugalList/FrugalListTestBase.cs | 14 ++++++++++---- .../FrugalList/ReadOnlyFrugalListTestBase.cs | 4 ++++ .../test/CommonUtilities.Test.csproj | 4 ++++ 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs b/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs index d1b983b..013ccc0 100644 --- a/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs +++ b/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs @@ -172,17 +172,9 @@ private class NonSeekableStream : Stream public override long Position { get; set; } public override void Flush() => throw new NotImplementedException(); - public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); - public override void SetLength(long value) => throw new NotImplementedException(); - + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); - - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite { get; } - public override long Length { get; } - public override long Position { get; set; } } } \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs index 037bc90..e91cd84 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs @@ -41,7 +41,9 @@ private FrugalList GenericFrugalListFactory(int count) public void Struct_Default() { var list = default(FrugalList); +#pragma warning disable xUnit2013 Assert.Equal(0, list.Count); +#pragma warning restore xUnit2013 Assert.False(list.IsReadOnly); } @@ -50,7 +52,9 @@ public void Constructor_Empty() { // ReSharper disable once CollectionNeverUpdated.Local var list = new FrugalList(); +#pragma warning disable xUnit2013 Assert.Equal(0, list.Count); +#pragma warning restore xUnit2013 Assert.False(list.IsReadOnly); } @@ -60,7 +64,9 @@ public void Constructor_Single() var t = CreateT(0); // ReSharper disable once CollectionNeverUpdated.Local var list = new FrugalList(t); +#pragma warning disable xUnit2013 Assert.Equal(1, list.Count); +#pragma warning restore xUnit2013 Assert.Equal(t, list[0]); Assert.False(list.IsReadOnly); } @@ -264,9 +270,9 @@ public void CopyByValue_SideEffects_OverrideLast(int count) [MemberData(nameof(ValidCollectionSizes))] public void ToList(int count) { - var enumerable = CreateEnumerable(null, count, 0, 0); + var enumerable = CreateEnumerable(null, count, 0, 0).ToList(); var list = new FrugalList(enumerable); - Assert.Equal(enumerable.ToList(), list.ToList()); + Assert.Equal(enumerable, list.ToList()); } @@ -327,7 +333,7 @@ public void ToArray(int count) public void GetEnumerator(int _, int enumerableLength, int __, int numberOfDuplicateElements) #pragma warning restore xUnit1026 { - var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); + var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements).ToList(); var list = new FrugalList(enumerable); var actualList = new List(); @@ -336,7 +342,7 @@ public void GetEnumerator(int _, int enumerableLength, int __, int numberOfDupli while (enumerator.MoveNext()) actualList.Add(enumerator.Current); - Assert.Equal(enumerable.ToList(), actualList); + Assert.Equal(enumerable, actualList); } #endregion diff --git a/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs index 6640abe..025838e 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs @@ -40,7 +40,9 @@ public void Empty_Idempotent() #pragma warning disable xUnit2002 Assert.NotNull(ReadOnlyFrugalList.Empty); #pragma warning restore xUnit2002 +#pragma warning disable xUnit2013 Assert.Equal(0, ReadOnlyFrugalList.Empty.Count); +#pragma warning restore xUnit2013 Assert.Equal(ReadOnlyFrugalList.Empty, ReadOnlyFrugalList.Empty); } @@ -59,7 +61,9 @@ public void Ctor_Single() { var t = CreateT(0); var list = new ReadOnlyFrugalList(t); +#pragma warning disable xUnit2013 Assert.Equal(1, list.Count); +#pragma warning restore xUnit2013 Assert.Equal(t, list[0]); } diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index fa8d08c..1005529 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -30,6 +30,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From d61485c0367ae50187f8eb8faa927a322174964a Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 16 Jan 2026 10:56:42 +0100 Subject: [PATCH 32/43] Async pipeline (#294) * implement a new async runner * start async pipeline * remove delays * implement step runners * adjust tests * add tests for properties * code doc * unify step tests * resolve warnings * fix potential deadlock in sequtial runner configuration * reimplement pipelines * reimplement step pipelines * remove debug relict * change target * test pielinerunner step * fix tests * documentation and code corrections * amend nullable annotation * resolve some warnings * switch from blockingcollection to channel * test more stable * add tests for correct cancellation and preparation errors with p/c pipeline * fix race with cancellation and adding steps to p/c pipeline * rename p/c pipeline * move some namepsaces * fix test * frugallist implements ireadonlycollection rofrugallist explicitly implements ilist * unify tests for RO lists/collections * start rewriting valuelist * replace ReadOnlyFrugalList by ImmutableFrugalList * generalize ValueListDictionary to support also non-Frugal implementations * use var * enumerator corrections * use is null * refactor tests and add missing tests * start testing ReadOnlyFrugalValueListDictionary * augment enumerator test * make ReadOnlyFrugalValueListDictionary closed to IReadOnlyFrugalValueListDictionary only * enable re organize tests * update deps * move to flie * rename count to ValueCount * add AddRange and RemoveAll * unify doc * update deps * document code * rename keycount to count * resolve warnings * flip the meaning return value of Add and added bool return to AddRange * revert theorydata * some corrections to formatting and docs * refactor AwaitExtensionsTests to use a helper for process creation and ensure TaskCompletionSource runs continuations asynchronously * update AwaitExtensionsTests to simplify process creation by using direct executable path --- .../CommonUtilities.DownloadManager.csproj | 8 +- .../src/DownloadFailedException.cs | 2 +- .../src/DownloadKind.cs | 2 +- .../src/DownloadManager.cs | 2 +- ...ommonUtilities.DownloadManager.Test.csproj | 10 +- .../src/Commonutilities.FileSystem.csproj | 12 +- .../src/FileSystemUtilities.cs | 2 +- .../src/Normalization/PathNormalizer.cs | 2 +- .../src/Utilities/ValueStringBuilder.cs | 15 +- .../CurrentSystemFileNameValidator.cs | 8 +- .../src/Validation/FileNameValidator.cs | 1 + .../Validation/WindowsFileNameValidator.cs | 2 +- .../CommonUtilities.FileSystem.Test.csproj | 8 +- .../src/CommonUtilities.Registry.csproj | 8 +- .../src/InMemoryRegistryChangeKind.cs | 2 +- .../src/InMemoryRegistryChangedEventArgs.cs | 3 +- .../test/CommonUtilities.Registry.Test.csproj | 8 +- .../src/CommonUtilities.SimplePipeline.csproj | 9 +- .../src/Extensions.cs | 20 +- .../src/IParallelStepRunner.cs | 33 - .../src/IStep.cs | 9 +- .../src/IStepRunner.cs | 65 + .../src/Pipelines/IPipeline.cs | 24 +- .../src/Pipelines/ParallelPipeline.cs | 30 - .../ParallelProducerConsumerPipeline.cs | 122 - .../src/Pipelines/Pipeline.cs | 247 +- .../src/Pipelines/ProducerConsumerPipeline.cs | 148 ++ .../src/Pipelines/SequentialPipeline.cs | 18 +- .../src/Pipelines/StepRunnerPipeline.cs | 96 +- .../src/Pipelines/StepRunnerPipelineBase.cs | 158 ++ .../Progress/AggregatedProgressReporter.cs | 3 +- .../src/Runners/AsyncStepRunner.cs | 274 +++ .../ParallelProducerConsumerStepRunner.cs | 66 - .../src/Runners/ParallelStepRunner.cs | 39 - .../src/Runners/ParallelStepRunnerBase.cs | 71 - .../src/Runners/ProducerConsumerStepRunner.cs | 124 + .../src/Runners/SequentialStepRunner.cs | 8 + .../src/Runners/StepRunner.cs | 47 - .../src/Runners/StepRunnerBase.cs | 143 -- .../src/StepFailureException.cs | 26 +- .../src/Steps/AsyncStep.cs | 86 - .../src/Steps/PipelineStep.cs | 91 +- .../src/Steps/RunPipelineStep.cs | 49 +- .../src/Steps/SynchronizedStep.cs | 78 - .../src/Steps/WaitStep.cs | 18 +- ...CommonUtilities.SimplePipeline.Test.csproj | 10 +- .../test/ExtensionsTest.cs | 105 +- .../test/Pipelines/ParallelPipelineTests.cs | 64 +- .../ParallelProducerConsumerPipelineTest.cs | 171 -- .../test/Pipelines/PipelineTest.cs | 177 +- .../test/Pipelines/PipelineTestBase.cs | 728 ++++++ .../Pipelines/ProducerConsumerPipelineTest.cs | 688 ++++++ .../test/Pipelines/RunnerBehavior.cs | 7 + .../test/Pipelines/SequentialPipelineTests.cs | 84 +- .../StepRunnerPipelineBaseTestBase.cs | 553 +++++ .../test/Pipelines/StepRunnerPipelineTest.cs | 97 - .../Pipelines/StepRunnerPipelineTestBase.cs | 54 + .../AggregatedProgressReporterTests.cs | 281 ++- .../test/Progress/ProgressEventArgsTest.cs | 1 + .../test/Runners/AsyncStepRunnerTest.cs | 21 + .../ParallelProducerConsumerStepRunnerTest.cs | 218 -- .../test/Runners/ParallelStepRunnerTest.cs | 69 - .../Runners/ParallelStepRunnerTestBase.cs | 184 -- .../Runners/ProducerConsumerStepRunnerTest.cs | 375 +++ .../test/Runners/SequentialStepRunnerTest.cs | 42 +- .../test/Runners/StepRunnerTestBase.cs | 2076 ++++++++++++++++- .../test/StepErrorEventArgsTest.cs | 4 +- .../test/StepFailureExceptionTests.cs | 87 +- .../test/Steps/PipelineStepTest.cs | 242 +- .../test/Steps/PipelineStepTestBase.cs | 400 ++++ .../test/Steps/RunPipelineStepTest.cs | 262 ++- .../test/Steps/SynchronizedStepTest.cs | 94 - .../test/Steps/WaitStepTest.cs | 126 +- .../test/{ => TestData}/TestStep.cs | 36 +- .../Collections/CollectionsTestSuite.cs | 13 +- .../IReadOnlyCollectionTestSuite.cs | 60 - .../Collections/IReadOnlyListTestSuite.cs | 113 - .../CommonUtilities.Testing.csproj | 12 +- .../ConstantHashCodeEqualityComparer.cs | 2 +- .../ReferenceEqualityComparer.cs | 4 +- .../Extensions/RandomExtensions.cs | 8 +- .../Extensions/StringExtensions.cs | 4 +- src/CommonUtilities.Testing/TestingHelpers.cs | 2 +- src/CommonUtilities/src/AwaitExtensions.cs | 2 +- .../src/Collections/DebugViews.cs | 8 +- .../src/Collections/EmptyEnumerator.cs | 25 + .../src/Collections/FrugalList.cs | 102 +- .../Collections/FrugalValueListDictionary.cs | 249 ++ .../Collections/IFrugalValueListDictionary.cs | 29 + .../IReadOnlyFrugalValueListDictionary.cs | 85 + .../IReadOnlyValueListDictionary.cs | 93 +- .../src/Collections/IValueListDictionary.cs | 47 +- ...lyFrugalList.cs => ImmutableFrugalList.cs} | 119 +- .../ReadOnlyFrugalValueListDictionary.cs | 58 + .../ReadOnlyValueListDictionary.cs | 138 +- .../ReadOnlyValueListDictionaryBase.cs | 154 ++ .../src/Collections/ValueListDictionary.cs | 631 +---- .../Collections/ValueListDictionaryBase.cs | 893 +++++++ .../src/CommonUtilities.csproj | 10 +- .../Providers/HashAlgorithmProviderBase.cs | 1 - src/CommonUtilities/src/TaskExtensions.cs | 4 +- .../test/AwaitExtensionsTests.cs | 42 +- .../test/Collections/DebugViewTest.cs | 25 +- .../Collections/FrugalList/FrugalListTest.cs | 45 +- .../FrugalList/FrugalListTestBase.cs | 17 +- .../FrugalList/FrugalListTestSuite.cs | 11 + ...Base.cs => ImmutableFrugalListTestBase.cs} | 220 +- ...stTests.cs => ImmutableFrugalListTests.cs} | 27 +- .../FrugalValueListDictionaryTest.Keys.cs | 30 + .../FrugalValueListDictionaryTest.Values.cs | 29 + .../FrugalValueListDictionaryTestBase.cs | 247 ++ .../Frugal/FrugalValueListDictionaryTests.cs | 38 + ...dOnlyFrugalValueListDictionaryTest.Keys.cs | 30 + ...nlyFrugalValueListDictionaryTest.Values.cs | 32 + ...adOnlyFrugalValueListDictionaryTestBase.cs | 323 +++ ...ReadOnlyFrugalValueListDictionaryTests.cs} | 11 +- .../ReadOnlyValueListDictionaryTest.Keys.cs | 25 + .../ReadOnlyValueListDictionaryTest.Values.cs | 27 + .../ReadOnlyValueListDictionaryTestBase.cs | 32 + .../IList/ReadOnlyValueListDictionaryTests.cs | 63 + .../IList/ValueListDictionaryTest.Keys.cs | 25 + .../IList/ValueListDictionaryTest.Values.cs | 25 + .../IList/ValueListDictionaryTestBase.cs | 27 + .../{ => IList}/ValueListDictionaryTests.cs | 7 +- .../IReadOnlyValueListDictionaryTestBase.cs | 167 +- .../IValueListDictionaryTestBase.cs | 441 +++- ...eadOnlyValueListDictionaryBaseTestSuite.cs | 180 ++ .../ReadOnlyValueListDictionaryTest.Keys.cs | 67 - .../ReadOnlyValueListDictionaryTest.Values.cs | 76 - .../ReadOnlyValueListDictionaryTestBase.cs | 94 - ...cs => ValueListDictionaryBaseTestSuite.cs} | 60 +- .../ValueListDictionaryTest.Keys.cs | 67 - .../ValueListDictionaryTest.Values.cs | 76 - ...alueListDictionaryTestSuite.Keys_Values.cs | 137 ++ .../test/CommonUtilities.Test.csproj | 8 +- .../test/Hashing/HashingServiceTest.cs | 10 +- 136 files changed, 11041 insertions(+), 4019 deletions(-) delete mode 100644 src/CommonUtilities.SimplePipeline/src/IParallelStepRunner.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelPipeline.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs create mode 100644 src/CommonUtilities.SimplePipeline/src/Pipelines/ProducerConsumerPipeline.cs create mode 100644 src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs create mode 100644 src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Runners/ParallelProducerConsumerStepRunner.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunner.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunnerBase.cs create mode 100644 src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs create mode 100644 src/CommonUtilities.SimplePipeline/src/Runners/SequentialStepRunner.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Runners/StepRunner.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Steps/AsyncStep.cs delete mode 100644 src/CommonUtilities.SimplePipeline/src/Steps/SynchronizedStep.cs delete mode 100644 src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs create mode 100644 src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestBase.cs create mode 100644 src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs create mode 100644 src/CommonUtilities.SimplePipeline/test/Pipelines/RunnerBehavior.cs create mode 100644 src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs delete mode 100644 src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTest.cs create mode 100644 src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs create mode 100644 src/CommonUtilities.SimplePipeline/test/Runners/AsyncStepRunnerTest.cs delete mode 100644 src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerStepRunnerTest.cs delete mode 100644 src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTest.cs delete mode 100644 src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTestBase.cs create mode 100644 src/CommonUtilities.SimplePipeline/test/Runners/ProducerConsumerStepRunnerTest.cs create mode 100644 src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestBase.cs delete mode 100644 src/CommonUtilities.SimplePipeline/test/Steps/SynchronizedStepTest.cs rename src/CommonUtilities.SimplePipeline/test/{ => TestData}/TestStep.cs (71%) delete mode 100644 src/CommonUtilities.Testing/Collections/IReadOnlyCollectionTestSuite.cs delete mode 100644 src/CommonUtilities.Testing/Collections/IReadOnlyListTestSuite.cs create mode 100644 src/CommonUtilities/src/Collections/EmptyEnumerator.cs create mode 100644 src/CommonUtilities/src/Collections/FrugalValueListDictionary.cs create mode 100644 src/CommonUtilities/src/Collections/IFrugalValueListDictionary.cs create mode 100644 src/CommonUtilities/src/Collections/IReadOnlyFrugalValueListDictionary.cs rename src/CommonUtilities/src/Collections/{ReadOnlyFrugalList.cs => ImmutableFrugalList.cs} (51%) create mode 100644 src/CommonUtilities/src/Collections/ReadOnlyFrugalValueListDictionary.cs create mode 100644 src/CommonUtilities/src/Collections/ReadOnlyValueListDictionaryBase.cs create mode 100644 src/CommonUtilities/src/Collections/ValueListDictionaryBase.cs create mode 100644 src/CommonUtilities/test/Collections/FrugalList/FrugalListTestSuite.cs rename src/CommonUtilities/test/Collections/FrugalList/{ReadOnlyFrugalListTestBase.cs => ImmutableFrugalListTestBase.cs} (59%) rename src/CommonUtilities/test/Collections/FrugalList/{ReadOnlyFrugalListTests.cs => ImmutableFrugalListTests.cs} (64%) create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTest.Keys.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTest.Values.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTestBase.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTests.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTest.Keys.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTest.Values.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTestBase.cs rename src/CommonUtilities/test/Collections/ValueListDictionary/{ReadOnlyValueListDictionaryTests.cs => Frugal/ReadOnlyFrugalValueListDictionaryTests.cs} (74%) create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTest.Keys.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTest.Values.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTestBase.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTests.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTest.Keys.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTest.Values.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTestBase.cs rename src/CommonUtilities/test/Collections/ValueListDictionary/{ => IList}/ValueListDictionaryTests.cs (92%) create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryBaseTestSuite.cs delete mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Keys.cs delete mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Values.cs delete mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs rename src/CommonUtilities/test/Collections/ValueListDictionary/{ValueListDictionaryTestBase.cs => ValueListDictionaryBaseTestSuite.cs} (55%) delete mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Keys.cs delete mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Values.cs create mode 100644 src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestSuite.Keys_Values.cs diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj index e80e4fc..059279d 100644 --- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj +++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj @@ -23,8 +23,8 @@ - - + + @@ -41,5 +41,9 @@ + + + + diff --git a/src/CommonUtilities.DownloadManager/src/DownloadFailedException.cs b/src/CommonUtilities.DownloadManager/src/DownloadFailedException.cs index bf5be32..c58d51a 100644 --- a/src/CommonUtilities.DownloadManager/src/DownloadFailedException.cs +++ b/src/CommonUtilities.DownloadManager/src/DownloadFailedException.cs @@ -38,7 +38,7 @@ public override string Message /// Initializes a new instance of the class from the specified download failures. /// /// The failures which occurred during a file download. - public DownloadFailedException(IEnumerable downloadFailures) + internal DownloadFailedException(IEnumerable downloadFailures) { DownloadFailures = downloadFailures; } diff --git a/src/CommonUtilities.DownloadManager/src/DownloadKind.cs b/src/CommonUtilities.DownloadManager/src/DownloadKind.cs index e50550c..932d1f2 100644 --- a/src/CommonUtilities.DownloadManager/src/DownloadKind.cs +++ b/src/CommonUtilities.DownloadManager/src/DownloadKind.cs @@ -14,5 +14,5 @@ public enum DownloadKind /// /// The provider supports downloading files from the Internet. /// - Internet, + Internet } \ No newline at end of file diff --git a/src/CommonUtilities.DownloadManager/src/DownloadManager.cs b/src/CommonUtilities.DownloadManager/src/DownloadManager.cs index 5e7a14c..8dd40c7 100644 --- a/src/CommonUtilities.DownloadManager/src/DownloadManager.cs +++ b/src/CommonUtilities.DownloadManager/src/DownloadManager.cs @@ -29,7 +29,7 @@ public sealed class DownloadManager : IDownloadManager /// /// Initializes a new instance of the class. /// - /// + /// The service provider. public DownloadManager(IServiceProvider serviceProvider) : this(DownloadManagerConfiguration.Default, serviceProvider) { diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj index 3652bc0..f91c118 100644 --- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj +++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj @@ -16,10 +16,10 @@ - - + + - + @@ -39,4 +39,8 @@ + + + + \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj index 7fd9e83..ac6a9e5 100644 --- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj +++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj @@ -25,11 +25,9 @@ - - - + + + @@ -43,4 +41,8 @@ + + + + diff --git a/src/CommonUtilities.FileSystem/src/FileSystemUtilities.cs b/src/CommonUtilities.FileSystem/src/FileSystemUtilities.cs index f9d2f73..2dc0ade 100644 --- a/src/CommonUtilities.FileSystem/src/FileSystemUtilities.cs +++ b/src/CommonUtilities.FileSystem/src/FileSystemUtilities.cs @@ -19,7 +19,7 @@ public static class FileSystemUtilities /// When set to , if all retries are unsuccessful the causing exception will be thrown. /// Callback which gets invoked if an /// or is was thrown during the execution. - /// if the operation was successful. otherwise. + /// if the operation was successful; otherwise, . /// is . public static bool ExecuteFileSystemActionWithRetry(int retryCount, int retryDelay, Action fileAction, bool throwOnFailure = true, Func? errorAction = null) diff --git a/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs b/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs index 8c1f3e3..bb99d03 100644 --- a/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs +++ b/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs @@ -6,7 +6,7 @@ namespace AnakinRaW.CommonUtilities.FileSystem.Normalization; /// -/// Enables customized path normalization. +/// Provides methods for normalizing file system paths according to specified rules. /// public static class PathNormalizer { diff --git a/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs b/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs index 88952b5..ffc9445 100644 --- a/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs +++ b/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs @@ -1,12 +1,14 @@ using System; using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace AnakinRaW.CommonUtilities.FileSystem.Utilities; // Copied from https://github.com/dotnet/runtime +[DebuggerDisplay("{DebuggerDisplay,nq}")] internal ref struct ValueStringBuilder { private char[]? _arrayToReturnToPool; @@ -69,8 +71,8 @@ public ref char GetPinnableReference(bool terminate) { if (terminate) { - EnsureCapacity(Length + 1); - _chars[Length] = '\0'; + EnsureCapacity(_pos + 1); + _chars[_pos] = '\0'; } return ref MemoryMarshal.GetReference(_chars); } @@ -84,6 +86,11 @@ public ref char this[int index] } } + // ToString() clears the builder, so we need a side-effect free debugger display. + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + [ExcludeFromCodeCoverage] + private string DebuggerDisplay => AsSpan().ToString(); + public override string ToString() { var s = _chars.Slice(0, _pos).ToString(); @@ -102,8 +109,8 @@ public ReadOnlySpan AsSpan(bool terminate) { if (terminate) { - EnsureCapacity(Length + 1); - _chars[Length] = '\0'; + EnsureCapacity(_pos + 1); + _chars[_pos] = '\0'; } return _chars.Slice(0, _pos); } diff --git a/src/CommonUtilities.FileSystem/src/Validation/CurrentSystemFileNameValidator.cs b/src/CommonUtilities.FileSystem/src/Validation/CurrentSystemFileNameValidator.cs index 3a67574..d07cb6c 100644 --- a/src/CommonUtilities.FileSystem/src/Validation/CurrentSystemFileNameValidator.cs +++ b/src/CommonUtilities.FileSystem/src/Validation/CurrentSystemFileNameValidator.cs @@ -12,6 +12,10 @@ public sealed class CurrentSystemFileNameValidator : FileNameValidator /// Returns a singleton instance of the class. /// public static readonly CurrentSystemFileNameValidator Instance = new(); + + private readonly FileNameValidator _innerValidator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? WindowsFileNameValidator.Instance + : LinuxFileNameValidator.Instance; private CurrentSystemFileNameValidator() { @@ -20,8 +24,6 @@ private CurrentSystemFileNameValidator() /// public override FileNameValidationResult IsValidFileName(ReadOnlySpan fileName) { - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? WindowsFileNameValidator.Instance.IsValidFileName(fileName) - : LinuxFileNameValidator.Instance.IsValidFileName(fileName); + return _innerValidator.IsValidFileName(fileName); } } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Validation/FileNameValidator.cs b/src/CommonUtilities.FileSystem/src/Validation/FileNameValidator.cs index 6ba24aa..81b3883 100644 --- a/src/CommonUtilities.FileSystem/src/Validation/FileNameValidator.cs +++ b/src/CommonUtilities.FileSystem/src/Validation/FileNameValidator.cs @@ -21,5 +21,6 @@ public FileNameValidationResult IsValidFileName(string? fileName) /// Checks whether a string represent a valid file name /// /// The string to validate. + /// The result of the validation. public abstract FileNameValidationResult IsValidFileName(ReadOnlySpan fileName); } \ No newline at end of file diff --git a/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs b/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs index 480f177..cf1a127 100644 --- a/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs +++ b/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs @@ -36,7 +36,7 @@ private WindowsFileNameValidator() /// /// The string to validate. /// Determines whether the check shall include Windows reserved file names (e.g, AUX, LPT1, etc.). - /// + /// if the file name is valid; otherwise, . public FileNameValidationResult IsValidFileName(ReadOnlySpan fileName, bool checkWindowsReservedNames) { if (fileName.Length == 0) diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj index 32064da..9ffe7cd 100644 --- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj +++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj @@ -15,9 +15,9 @@ - + - + @@ -45,4 +45,8 @@ + + + + diff --git a/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj b/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj index 7a6efaa..e9e6ffb 100644 --- a/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj +++ b/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj @@ -23,8 +23,8 @@ - - + + @@ -34,4 +34,8 @@ + + + + diff --git a/src/CommonUtilities.Registry/src/InMemoryRegistryChangeKind.cs b/src/CommonUtilities.Registry/src/InMemoryRegistryChangeKind.cs index 195b396..dc33386 100644 --- a/src/CommonUtilities.Registry/src/InMemoryRegistryChangeKind.cs +++ b/src/CommonUtilities.Registry/src/InMemoryRegistryChangeKind.cs @@ -4,5 +4,5 @@ internal enum InMemoryRegistryChangeKind { TreeCreate, TreeDelete, - Value, + Value } \ No newline at end of file diff --git a/src/CommonUtilities.Registry/src/InMemoryRegistryChangedEventArgs.cs b/src/CommonUtilities.Registry/src/InMemoryRegistryChangedEventArgs.cs index f0bab56..4b3d0f6 100644 --- a/src/CommonUtilities.Registry/src/InMemoryRegistryChangedEventArgs.cs +++ b/src/CommonUtilities.Registry/src/InMemoryRegistryChangedEventArgs.cs @@ -2,7 +2,8 @@ namespace AnakinRaW.CommonUtilities.Registry; -internal class InMemoryRegistryChangedEventArgs(InMemoryRegistryKeyData key, InMemoryRegistryChangeKind kind) : EventArgs +internal sealed class InMemoryRegistryChangedEventArgs(InMemoryRegistryKeyData key, InMemoryRegistryChangeKind kind) + : EventArgs { public InMemoryRegistryKeyData KeyData { get; } = key; public InMemoryRegistryChangeKind Kind { get; } = kind; diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj index a9c4a3d..227e810 100644 --- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj +++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj @@ -26,9 +26,9 @@ - + - + @@ -48,4 +48,8 @@ + + + + diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj index 285e852..cc8f91c 100644 --- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj +++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj @@ -23,8 +23,9 @@ - - + + + @@ -46,4 +47,8 @@ + + + + \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Extensions.cs b/src/CommonUtilities.SimplePipeline/src/Extensions.cs index 12dbb89..946ace9 100644 --- a/src/CommonUtilities.SimplePipeline/src/Extensions.cs +++ b/src/CommonUtilities.SimplePipeline/src/Extensions.cs @@ -1,13 +1,31 @@ using System; +using System.Collections.Generic; using System.Linq; namespace AnakinRaW.CommonUtilities.SimplePipeline; internal static class Extensions { + /// + /// Throws a if any of the provided steps have failed. + /// + /// The collection of executed steps to evaluate for failures. + /// + /// Thrown when one or more steps in have failed, + /// excluding those which represent a cancelled Step. + /// + internal static void ThrowStepFailureExceptionForFailedSteps(this IEnumerable executedSteps) + { + var failedBuildSteps = executedSteps + .Where(p => p.Error != null && !p.Error.IsExceptionType()) + .ToList(); + if (failedBuildSteps.Count > 0) + throw new StepFailureException(failedBuildSteps); + } + extension(Exception error) { - public bool IsExceptionType() where T : Exception + internal bool IsExceptionType() where T : Exception { return error switch { diff --git a/src/CommonUtilities.SimplePipeline/src/IParallelStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/IParallelStepRunner.cs deleted file mode 100644 index 09b7bad..0000000 --- a/src/CommonUtilities.SimplePipeline/src/IParallelStepRunner.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace AnakinRaW.CommonUtilities.SimplePipeline; - -/// -/// A specialized which allows for synchronous waiting. -/// -public interface IParallelStepRunner : IStepRunner -{ - /// - /// Gets an aggregated exception of all failed steps or if no step failed. - /// - public AggregateException? Exception { get; } - - /// - /// Gets the number of parallel workers the uses. - /// - public int WorkerCount { get; } - - /// - /// Synchronously waits for this stepRunner for all of its steps to be finished. - /// - /// If any of the steps failed with an exception. - void Wait(); - - /// - /// Synchronously waits for this stepRunner for all of its steps to be finished. - /// - /// The time duration to wait. - /// If expired. - /// If any of the steps failed with an exception. - void Wait(TimeSpan waitDuration); -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/IStep.cs b/src/CommonUtilities.SimplePipeline/src/IStep.cs index 112be06..905bd89 100644 --- a/src/CommonUtilities.SimplePipeline/src/IStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/IStep.cs @@ -1,5 +1,7 @@ using System; +using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; namespace AnakinRaW.CommonUtilities.SimplePipeline; @@ -17,5 +19,10 @@ public interface IStep : IDisposable /// Run the step's action. /// /// Provided to allow step cancellation. - void Run(CancellationToken token); + Task RunAsync(CancellationToken token); + + /// + /// Gets an awaiter used to await this . + /// + TaskAwaiter GetAwaiter(); } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs index 415fff5..2816656 100644 --- a/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs +++ b/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -15,6 +16,34 @@ public interface IStepRunner /// event EventHandler? Error; + /// + /// Gets an aggregated exception of all failed steps or if no step failed. + /// + public AggregateException? Exception { get; } + + /// + /// Gets the number of parallel workers the uses. + /// + public int WorkerCount { get; } + + /// + /// Gets a value indicating whether the is currently executing steps. + /// + /// + /// if the runner is actively executing steps; otherwise, . + /// + bool IsRunning { get; } + + /// + /// Gets a value indicating whether the step runner has been cancelled. + /// + /// + /// This property returns if the step runner was cancelled during its execution, + /// typically due to a cancellation request via a . + /// Otherwise, it returns . + /// + bool IsCancelled { get; } + /// /// Gets a read-only list of only those steps were executed by the . /// @@ -33,4 +62,40 @@ public interface IStepRunner /// The step to app. /// /// is . void AddStep(IStep step); + + /// + /// Synchronously waits for this stepRunner for all of its steps to be finished. + /// + /// If any of the steps failed with an exception. + void Wait(); + + /// + /// Synchronously waits for this stepRunner for all of its steps to be finished. + /// + /// The time duration to wait. + /// If expired. + /// If any of the steps failed with an exception. + void Wait(TimeSpan waitDuration); + + /// + /// Gets an awaiter used to await this . + /// + /// + /// + /// This method enables the to be used with the await keyword. + /// + /// + /// If called before has been invoked, the awaiter will block until + /// the runner is started and has completed execution of all steps. + /// + /// + /// If called during execution, the awaiter will block until all steps have finished. + /// + /// + /// The awaiter does not throw exceptions for failed steps. Any errors that occurred during + /// execution are available through the property. + /// + /// + /// A instance that can be used to await the runner's completion. + TaskAwaiter GetAwaiter(); } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/IPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/IPipeline.cs index 469a948..db3341f 100644 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/IPipeline.cs +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/IPipeline.cs @@ -5,26 +5,24 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline; /// -/// Represents an execution pipeline can run multiple instanes. +/// Represents an execution pipeline that can be prepared and run. /// public interface IPipeline : IDisposable -{ +{ /// /// Prepares the pipeline for execution. /// - /// - /// Preparation can only be done once per instance. - /// - /// A task that completes when the preparation is completed. - Task PrepareAsync(); - + /// Token to cancel the preparation. + /// Cancellation was requested. + /// The pipeline was disposed. + Task PrepareAsync(CancellationToken token = default); + /// - /// Runs pipeline synchronously. + /// Runs the pipeline asynchronously. /// - /// Provided to allow cancellation. - /// A task that represents the operation completion. - /// The pipeline was cancelled was requested for cancellation. - /// The pipeline may throw this exception if one or many steps failed. + /// Token to cancel the execution. + /// Cancellation was requested. + /// The pipeline was disposed. Task RunAsync(CancellationToken token = default); /// diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelPipeline.cs deleted file mode 100644 index 77d7d58..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelPipeline.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using AnakinRaW.CommonUtilities.SimplePipeline.Runners; - -namespace AnakinRaW.CommonUtilities.SimplePipeline; - -/// -/// A simple pipeline that runs all steps on the thread pool in parallel. -/// -public abstract class ParallelPipeline : StepRunnerPipeline -{ - private readonly int _workerCount; - - /// - /// Initializes a new instance of the class. - /// - /// The service provider for dependency injection within the pipeline. - /// The number of worker threads to be used for parallel execution. - /// A value indicating whether the pipeline should fail fast. - /// is . - protected ParallelPipeline(IServiceProvider serviceProvider, int workerCount = 4, bool failFast = true) : base(serviceProvider, failFast) - { - _workerCount = workerCount; - } - - /// - protected sealed override ParallelStepRunner CreateRunner() - { - return new ParallelStepRunner(_workerCount, ServiceProvider); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs deleted file mode 100644 index 78dcb1e..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.SimplePipeline.Runners; - -namespace AnakinRaW.CommonUtilities.SimplePipeline; - -/// -/// A simple pipeline that runs all steps on the thread pool in parallel. Allows to run the pipeline even if preparation is not completed. -/// -/// -/// Useful, if preparation is work intensive. -/// -public abstract class ParallelProducerConsumerPipeline : Pipeline -{ - private readonly ParallelProducerConsumerStepRunner _stepRunner; - - private Exception? _preparationException; - - /// - protected override bool FailFast { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The service provider for dependency injection within the pipeline. - /// The number of worker threads to be used for parallel execution. - /// A value indicating whether the pipeline should fail fast. - protected ParallelProducerConsumerPipeline(int workerCount, bool failFast, IServiceProvider serviceProvider) : base(serviceProvider) - { - FailFast = failFast; - _stepRunner = new ParallelProducerConsumerStepRunner(workerCount, serviceProvider); - } - - /// - public sealed override async Task RunAsync(CancellationToken token = default) - { - ThrowIfDisposed(); - - LinkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); - - if (!Prepared) - { - Task.Run(async () => - { - try - { - await PrepareAsync().ConfigureAwait(false); - } - catch (Exception e) - { - PipelineFailed = true; - _preparationException = e; - - if (FailFast) - Cancel(); - } - finally - { - _stepRunner.Finish(); - } - }, LinkedCancellationTokenSource.Token).Forget(); - } - - try - { - await RunCoreAsync(LinkedCancellationTokenSource.Token).ConfigureAwait(false); - LinkedCancellationTokenSource.Token.ThrowIfCancellationRequested(); - } - catch (Exception) - { - PipelineFailed = true; - throw; - } - finally - { - if (LinkedCancellationTokenSource is not null) - { - LinkedCancellationTokenSource.Dispose(); - LinkedCancellationTokenSource = null; - } - } - } - - /// - /// Builds the steps in the order they should be executed within the pipeline. - /// - /// A list of steps in the order they should be executed. - protected abstract IAsyncEnumerable BuildSteps(); - - /// - protected override async Task PrepareCoreAsync() - { - await foreach (var step in BuildSteps().ConfigureAwait(false)) - _stepRunner.AddStep(step); - _stepRunner.Finish(); - return true; - } - - /// - protected override async Task RunCoreAsync(CancellationToken token) - { - try - { - _stepRunner.Error += OnError!; - await _stepRunner.RunAsync(token).ConfigureAwait(false); - } - finally - { - _stepRunner.Error -= OnError!; - } - - if (!PipelineFailed) - return; - - if (_preparationException is not null) - throw _preparationException; - - ThrowIfAnyStepsFailed(_stepRunner.ExecutedSteps); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/Pipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/Pipeline.cs index 0265123..a0842c8 100644 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/Pipeline.cs +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/Pipeline.cs @@ -1,158 +1,243 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AnakinRaW.CommonUtilities.SimplePipeline; /// -/// Base implementation for an +/// Base implementation for an . /// public abstract class Pipeline : DisposableObject, IPipeline { + private Task? _preparationTask; + private Task? _runTask; + +#if NET10_0_OR_GREATER + private readonly Lock _reentryLock = new(); +#else + private readonly object _reentryLock = new(); +#endif + /// - /// The cancellation token source used by this pipeline to send cancellation request. + /// Gets the used to cancel the execution of the pipeline. + /// Returns if the execution is not started or already finished. /// - protected CancellationTokenSource? LinkedCancellationTokenSource; - + protected CancellationTokenSource? CancellationTokenSource; + /// - /// Returns the service provider of the . + /// Gets the for the pipeline. /// protected readonly IServiceProvider ServiceProvider; - + /// - /// Returns the logger of the . + /// Gets the for the pipeline or if no logger is registered. /// protected readonly ILogger? Logger; /// - /// Gets a value indicating whether the preparation of the was successful. + /// Gets a value indicating whether the pipeline has been successfully prepared. /// - protected bool Prepared { get; set; } + /// + /// if the pipeline preparation task has completed successfully; otherwise, . + /// + protected internal bool IsPrepared => +#if NETSTANDARD2_0 || NETFRAMEWORK + _preparationTask is { Status: TaskStatus.RanToCompletion, IsCompleted: true }; +#else + _preparationTask?.IsCompletedSuccessfully is true; +#endif /// - /// Gets a value indicating whether the execution of the pipeline has encountered a failure. + /// Gets a value indicating whether the pipeline has encountered a failure during its execution. /// - public bool PipelineFailed { get; protected set; } - + /// + /// This property is set to if an exception occurs during the execution of the pipeline. + /// + public bool Failed { get; protected set; } + /// - /// Gets a value indicating the pipeline shall abort execution on the first received error. + /// Gets a value indicating whether the pipeline has been cancelled. /// - protected virtual bool FailFast => false; - + /// + /// This property is set to when the pipeline is explicitly cancelled + /// or when an is thrown during execution. + /// + public bool Cancelled { get; protected set; } + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with the specified service provider. /// + /// The used to resolve dependencies for the pipeline. /// is . protected Pipeline(IServiceProvider serviceProvider) { ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); Logger = serviceProvider.GetService()?.CreateLogger(GetType()); } - - /// - public async Task PrepareAsync() + + /// + /// Returns a string representation of the current instance. + /// + /// + /// A that represents the name of the current pipeline type. + /// + [ExcludeFromCodeCoverage] + public override string ToString() => GetType().Name; + + /// + /// Prepares the pipeline for execution. + /// + /// A to observe while waiting for the preparation to complete. + /// A that represents the asynchronous preparation operation. + /// The pipeline already is prepared or preparation has been started. + /// The pipeline has been disposed. + /// The operation is canceled. + public Task PrepareAsync(CancellationToken token = default) { ThrowIfDisposed(); - if (!Prepared) + token.ThrowIfCancellationRequested(); + lock (_reentryLock) { + if (_preparationTask is not null) + throw new InvalidOperationException("Pipeline preparation has already been started."); + try { - await PrepareCoreAsync().ConfigureAwait(false); + _preparationTask = PrepareCoreAsync(token); } - finally + catch (Exception ex) { - Prepared = true; + _preparationTask = Task.FromException(ex); + throw; } + return _preparationTask; } } - /// - public virtual async Task RunAsync(CancellationToken token = default) + /// + /// Executes the pipeline asynchronously. + /// + /// A to observe while waiting for the task to complete. + /// A that represents the asynchronous operation. + /// Thrown when the pipeline has already been started or is executing. + /// Thrown when the pipeline has been disposed. + public Task RunAsync(CancellationToken token = default) { ThrowIfDisposed(); - - await PrepareAsync().ConfigureAwait(false); - - try + lock (_reentryLock) { - try - { - LinkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); - await RunCoreAsync(LinkedCancellationTokenSource.Token).ConfigureAwait(false); - LinkedCancellationTokenSource.Token.ThrowIfCancellationRequested(); - } - finally - { - if (LinkedCancellationTokenSource is not null) - { - LinkedCancellationTokenSource.Dispose(); - LinkedCancellationTokenSource = null; - } - } - } - catch (Exception) - { - PipelineFailed = true; - throw; + if (_runTask is not null) + throw new InvalidOperationException("Pipeline has already been started."); + + _runTask = RunCoreAsync(token); + return _runTask; } } - /// + /// + /// Cancels the execution of the pipeline. + /// + /// + /// This method ensures that the pipeline's execution is stopped by canceling the associated + /// . Once canceled, the property is set to true. + /// + /// Thrown if the pipeline or its associated resources have already been disposed. public void Cancel() { - LinkedCancellationTokenSource?.Cancel(); - } - - /// - [ExcludeFromCodeCoverage] - public override string ToString() - { - return GetType().Name; + var cts = CancellationTokenSource; + if (cts != null) + { + cts.Cancel(); + Cancelled = true; + } } /// /// Performs the actual preparation of this instance. /// - /// if the planning was successful; otherwise. - protected abstract Task PrepareCoreAsync(); + protected abstract Task PrepareCoreAsync(CancellationToken token); /// - /// Implements the run logic of this instance. + /// Implements the actual execution logic of this instance. /// /// It's assured this instance is already prepared when this method gets called. - /// Provided to allow cancellation. - protected abstract Task RunCoreAsync(CancellationToken token); + protected abstract Task ExecuteAsync(CancellationToken token); /// - /// Throws an if any of the passed steps ended with an error that is not the result of cancellation. + /// Orchestrates and executes the pipeline. /// - /// The steps that were executed by the pipeline. - /// If any of has an error that is not the result of cancellation. - protected void ThrowIfAnyStepsFailed(IEnumerable steps) + /// + /// + /// Override this method to customize execution flow. + /// + /// + /// The default implementation calls if needed, + /// sets up cancellation, and delegates to . + /// + /// + protected virtual async Task RunCoreAsync(CancellationToken token) { - var failedBuildSteps = steps - .Where(p => p.Error != null && !p.Error.IsExceptionType()) - .ToList(); + CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + + try + { + await WaitForPreparationAsync(CancellationTokenSource.Token).ConfigureAwait(false); + await ExecuteAsync(CancellationTokenSource.Token).ConfigureAwait(false); + CancellationTokenSource.Token.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException) + { + Cancelled = true; + throw; + } + catch + { + Failed = true; + throw; + } + finally + { + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; + } + } - if (failedBuildSteps.Any()) - throw new StepFailureException(failedBuildSteps); + /// + /// Releases resources used by the pipeline, including any tasks and cancellation tokens. + /// + protected override void DisposeResources() + { + lock (_reentryLock) + { + _preparationTask?.Dispose(); + _runTask?.Dispose(); + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; + + // Safe because we explicitly check the methods if disposed + _preparationTask = null; + _runTask = null; + } + + base.DisposeResources(); } /// - /// The default event handler that can be used when an error occurs within a step. - /// is set to . When is , the pipeline gets cancelled. + /// Waits for preparation or starts it if not yet started. /// - /// The sender of the event. - /// The event arguments. - protected virtual void OnError(object sender, StepRunnerErrorEventArgs e) + protected Task WaitForPreparationAsync(CancellationToken token) { - PipelineFailed = true; - if (FailFast || e.Cancel) - Cancel(); + Task task; + + lock (_reentryLock) + { + _preparationTask ??= PrepareCoreAsync(token); + task = _preparationTask; + } + return task.WaitAsync(token); + } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/ProducerConsumerPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/ProducerConsumerPipeline.cs new file mode 100644 index 0000000..2f73a94 --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/ProducerConsumerPipeline.cs @@ -0,0 +1,148 @@ +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AnakinRaW.CommonUtilities.SimplePipeline; + +/// +/// A pipeline that runs preparation and execution in parallel using a producer/consumer pattern. +/// +/// +/// Steps are added to the runner while execution is already in progress. +/// Useful when preparation is work-intensive. +/// +public abstract class ProducerConsumerPipeline : StepRunnerPipelineBase +{ + private readonly int _workerCount; + + /// + /// Initializes a new instance of the class with the specified worker count and service provider. + /// + /// The number of workers to be used in the pipeline. Must be between 1 and 64 inclusive. + /// The used to resolve dependencies for the pipeline. + /// is less than 1 or greater than 64. + /// is . + protected ProducerConsumerPipeline(int workerCount, IServiceProvider serviceProvider) : base(serviceProvider) + { + if (workerCount is < 1 or > 64) + throw new ArgumentOutOfRangeException(nameof(workerCount), "worker count must be between 1 and 64 inclusive"); + _workerCount = workerCount; + } + + /// + /// Asynchronously builds a collection of steps to be executed by the pipeline. + /// + /// A to observe while waiting for the steps to be built. + /// An asynchronous enumerable of instances representing the steps to be executed. + /// + /// This method is intended to be overridden in derived classes to provide the logic for preparing the steps + /// that will be executed by the pipeline. The steps are produced asynchronously, allowing for efficient + /// preparation of steps in scenarios where preparation is computationally intensive or involves I/O operations. + /// + protected abstract IAsyncEnumerable BuildStepsAsync(CancellationToken token); + + /// + /// Creates an instance of to execute the steps in the pipeline using a producer/consumer pattern. + /// + /// + /// The is initialized with the specified number of workers and the service provider. + /// This method ensures that the runner is properly configured to handle the producer/consumer pattern for step execution. + /// + /// + /// A configured instance of . + /// + protected sealed override ProducerConsumerStepRunner CreateRunner() + { + return new ProducerConsumerStepRunner(_workerCount, ServiceProvider); + } + + /// + /// Prepares the pipeline by initializing the step runner and adding steps to it asynchronously. + /// + /// A to observe while waiting for the preparation to complete. + /// A that represents the asynchronous preparation operation. + /// + /// This method initializes the step runner and asynchronously builds and adds steps to the runner. + /// Once all steps are added, the runner is marked as finished. + /// + protected sealed override async Task PrepareCoreAsync(CancellationToken token) + { + _ = StepRunner; + await foreach (var step in BuildStepsAsync(token).ConfigureAwait(false)) + { + token.ThrowIfCancellationRequested(); + if (!StepRunner.TryAddStep(step) && !Cancelled) + throw new InvalidOperationException("Unable to add write steps to underlying runner"); + } + StepRunner.Finish(); + } + + /// + /// Executes the core logic of the pipeline asynchronously. + /// + /// A to observe while waiting for the task to complete. + /// A that represents the asynchronous operation. + /// + /// This method initializes the runner, links the provided cancellation token, and ensures the pipeline gets prepared. + /// It handles cancellation and failure scenarios, ensuring proper cleanup of resources. + /// + /// Thrown when the operation is canceled. + /// Thrown when an error occurs during execution. + protected sealed override async Task RunCoreAsync(CancellationToken token) + { + CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + var linkedToken = CancellationTokenSource.Token; + + if (!IsPrepared) + { + Task.Run(() => RunPreparationAsync(linkedToken), CancellationToken.None).Forget(); + } + + try + { + await ExecuteAsync(linkedToken).ConfigureAwait(false); + await WaitForPreparationAsync(token).ConfigureAwait(false); + linkedToken.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException) + { + Cancelled = true; + throw; + } + catch + { + Failed = true; + throw; + } + finally + { + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; + } + } + + private async Task RunPreparationAsync(CancellationToken token) + { + try + { + await WaitForPreparationAsync(token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Cancelled = true; + Cancel(); + } + catch (Exception) + { + Failed = true; + if (FailFast) + Cancel(); + } + finally + { + StepRunner.Finish(); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/SequentialPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/SequentialPipeline.cs index 97e2a29..421f712 100644 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/SequentialPipeline.cs +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/SequentialPipeline.cs @@ -6,20 +6,28 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline; /// /// A simple pipeline that runs all steps sequentially. /// -public abstract class SequentialPipeline : StepRunnerPipeline +public abstract class SequentialPipeline : StepRunnerPipeline { /// /// Initializes a new instance of the class. /// /// The service provider for dependency injection within the pipeline. - /// A value indicating whether the pipeline should fail fast. /// is . - protected SequentialPipeline(IServiceProvider serviceProvider, bool failFast = true) : base(serviceProvider, failFast) + protected SequentialPipeline(IServiceProvider serviceProvider) : base(serviceProvider) { } - /// - protected sealed override SequentialStepRunner CreateRunner() + /// + /// Creates an instance of to execute the steps in the pipeline sequentially. + /// + /// + /// This method returns a . + /// The runner ensures that all steps are executed one after another in a sequential manner. + /// + /// + /// An instance of that executes steps sequentially. + /// + protected sealed override IStepRunner CreateRunner() { return new SequentialStepRunner(ServiceProvider); } diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs index 8087d43..8b1cf9d 100644 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs @@ -1,84 +1,64 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; namespace AnakinRaW.CommonUtilities.SimplePipeline; /// -/// Base class for a pipeline implementation utilizing an as its primary execution engine. +/// Base class for pipelines that use an with sequential preparation and execution. /// -/// The type of the step stepRunner. -public abstract class StepRunnerPipeline : Pipeline where TRunner : IStepRunner +/// +/// +/// This class follows a sequential pattern: preparation completes fully before execution begins. +/// +/// +/// For pipelines that need to run preparation and execution in parallel (producer/consumer pattern), +/// use instead. +/// +/// +public abstract class StepRunnerPipeline(IServiceProvider serviceProvider) : StepRunnerPipelineBase(serviceProvider) { - private IStepRunner _buildStepRunner = null!; - - /// - protected override bool FailFast { get; } - /// - /// Initializes a new instance of the class. + /// Creates a collection of steps to be executed by the . /// - /// The service provider the pipeline. - /// A value indicating whether the pipeline should fail fast. + /// + /// A that can be used to cancel the step creation process. + /// + /// + /// A task that represents the asynchronous operation. The task result contains a list of steps + /// () to be added to the . + /// /// - /// The parameter determines whether the pipeline should stop executing immediately upon encountering the first failure. + /// This method is abstract and must be implemented by derived classes to define the specific steps + /// required for the pipeline. The steps are created during the preparation phase of the pipeline. /// - /// is . - protected StepRunnerPipeline(IServiceProvider serviceProvider, bool failFast = true) : base(serviceProvider) - { - FailFast = failFast; - } - - /// - [ExcludeFromCodeCoverage] - public override string ToString() - { - return GetType().Name; - } - - /// - /// Creates the step stepRunner for the pipeline. - /// - /// The step stepRunner instance. - protected abstract TRunner CreateRunner(); + protected abstract Task> CreateRunnerSteps(CancellationToken token); /// - /// Builds the steps that should be executed within the pipeline. + /// Prepares the pipeline by initializing the step runner and adding the steps to be executed. /// + /// A to observe while waiting for the task to complete. /// - /// The order of the steps might be relevant, depending on the type of . + /// This method initializes the step runner and sequentially adds the steps created by + /// to the runner. It ensures that the preparation phase + /// is fully completed before the execution phase begins. /// - /// A task that returns a list of steps. - protected abstract Task> BuildSteps(); - - /// - protected override async Task PrepareCoreAsync() + /// + /// The step runner is not properly initialized before adding steps. + /// + /// A that represents the asynchronous operation. + protected sealed override async Task PrepareCoreAsync(CancellationToken token) { - _buildStepRunner = CreateRunner() ?? throw new InvalidOperationException("RunnerFactory created null value!"); - var steps = await BuildSteps().ConfigureAwait(false); - foreach (var step in steps) - _buildStepRunner.AddStep(step); - return true; + _ = StepRunner; + var steps = await CreateRunnerSteps(token).ConfigureAwait(false); + foreach (var step in steps) + StepRunner.AddStep(step); } /// - protected override async Task RunCoreAsync(CancellationToken token) + protected sealed override Task RunCoreAsync(CancellationToken token) { - try - { - _buildStepRunner.Error += OnError!; - await _buildStepRunner.RunAsync(token).ConfigureAwait(false); - } - finally - { - _buildStepRunner.Error -= OnError!; - } - - if (!PipelineFailed) - return; - - ThrowIfAnyStepsFailed(_buildStepRunner.ExecutedSteps); + return base.RunCoreAsync(token); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs new file mode 100644 index 0000000..f82afec --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs @@ -0,0 +1,158 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AnakinRaW.CommonUtilities.SimplePipeline; + +/// +/// Represents a base class for pipelines that execute steps using a . +/// +/// The type of the step runner used to execute the steps. +/// +/// +/// Running the pipeline may throw a if one or many steps produce errors. +/// +/// +/// This class provides functionality for managing a step runner, executing steps, handling errors, +/// and disposing resources. Derived classes must implement the method +/// to provide a specific step runner implementation. +/// +/// +public abstract class StepRunnerPipelineBase : Pipeline where TStepRunner : class, IStepRunner +{ + private readonly Lazy _stepRunnerLazy; + + /// + /// Initializes a new instance of the class with the specified service provider. + /// + /// The used to resolve dependencies for the pipeline. + /// is . + protected StepRunnerPipelineBase(IServiceProvider serviceProvider) : base(serviceProvider) + { + _stepRunnerLazy = new Lazy(EnsureRunner, LazyThreadSafetyMode.ExecutionAndPublication); + } + + /// + /// Gets the step runner used to execute the steps in the pipeline. + /// + /// + /// The step runner of type . + /// + /// + /// + /// The step runner is lazily initialized when accessed for the first time, using . + /// It is guaranteed to be non- once initialized. + /// + /// + /// Derived classes can use this property to add steps to the runner or to + /// perform operations specific to the step runner implementation. + /// + /// + protected internal TStepRunner StepRunner => _stepRunnerLazy.Value; + + private TStepRunner EnsureRunner() + { + return CreateRunner() ?? throw new InvalidOperationException("CreateRunner must not return null."); + } + + /// + /// Gets a value indicating whether the step runner has been initialized. + /// + /// + /// if the step runner has been initialized; otherwise, . + /// + /// + /// When this property returns , the property is guaranteed + /// to return a non- value. + /// + protected internal bool IsStepRunnerInitialized => _stepRunnerLazy.IsValueCreated; + + /// + /// Gets or sets a value indicating whether the pipeline should terminate execution immediately + /// upon encountering an error. + /// + /// + /// if the pipeline should stop execution on the first error; otherwise, . + /// + /// + /// When set to , the pipeline will cancel further processing as soon as an error occurs. + /// This is useful for scenarios where continuing execution after an error is not desirable. + /// + public bool FailFast { get; protected set; } = false; + + /// + /// Creates an instance of the step runner used to execute the steps in the pipeline. + /// + /// An instance of representing the step runner. + /// + /// Derived classes must implement this method to provide a specific implementation of the step runner. + /// The returned step runner must not be + /// + protected abstract TStepRunner CreateRunner(); + + /// + /// Executes the pipeline asynchronously, running all steps added to . + /// + /// A to observe while waiting for the task to complete. + /// + /// A that represents the asynchronous execution of the pipeline. + /// + /// + /// This method attaches an error handler to the step runner, executes the steps asynchronously, + /// and ensures that any failed steps throw an exception after execution. + /// + /// + /// Thrown when if any executed step failed excluding those which represent a cancelled Step. + /// + protected sealed override async Task ExecuteAsync(CancellationToken token) + { + try + { + StepRunner.Error += OnRunnerExecutionError!; + await StepRunner.RunAsync(token).ConfigureAwait(false); + } + finally + { + StepRunner.Error -= OnRunnerExecutionError!; + } + StepRunner.ExecutedSteps.ThrowStepFailureExceptionForFailedSteps(); + } + + /// + /// Handles errors that occur during the execution of the . + /// + /// The source of the event, typically the instance. + /// The containing details about the error. + /// + /// This method updates the pipeline's state based on the error details, including whether the pipeline + /// should be cancelled or marked as failed. If is enabled or the error indicates + /// cancellation, the pipeline will be cancelled immediately. + /// + protected virtual void OnRunnerExecutionError(object sender, StepRunnerErrorEventArgs e) + { + var isCancel = IsCancel(e); + Cancelled |= isCancel; + Failed |= !isCancel; + + if (FailFast || isCancel) + Cancel(); + } + + private static bool IsCancel(StepRunnerErrorEventArgs e) + { + return e.Cancel || e.Exception.IsExceptionType(); + } + + /// + /// Releases the resources used by the instance. + /// + /// + /// This method ensures that any resources associated with the pipeline, including the step runner, are properly disposed of. + /// + protected override void DisposeResources() + { + base.DisposeResources(); + if (IsStepRunnerInitialized) + StepRunner.Error -= OnRunnerExecutionError!; + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs b/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs index 95246ae..c5760fb 100644 --- a/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs +++ b/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs @@ -117,8 +117,7 @@ protected AggregatedProgressReporter( private void OnStepProgress(object sender, ProgressEventArgs e) { - if (sender is not TStep step) - throw new InvalidCastException($"Cannot cast '{sender.GetType()}' to {typeof(TStep)}"); + var step = (TStep)sender; if (!_progressSteps.Contains(step)) return; diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs new file mode 100644 index 0000000..6182e70 --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs @@ -0,0 +1,274 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; + +/// +/// Represents an asynchronous step runner that manages the execution of steps in a pipeline. +/// +/// +/// This class provides functionality to add steps, execute them asynchronously, and handle errors during execution. +/// It supports multiple workers for parallel step execution and ensures proper cancellation and error handling. +/// +public class AsyncStepRunner : IStepRunner +{ + /// + public event EventHandler? Error; + + private readonly ConcurrentQueue _pendingSteps = new(); + private readonly ConcurrentBag _executedSteps = []; + private readonly ConcurrentBag _exceptions = []; + private readonly TaskCompletionSource _completionSource = new(); + + /// + public AggregateException? Exception => _exceptions.IsEmpty ? null : new AggregateException(_exceptions); + + /// + /// Gets a value indicating whether the steps in the pipeline are executed sequentially. + /// + /// + /// if the steps are executed sequentially; otherwise, . + /// + /// + /// The execution is considered sequential when the is set to 1. + /// + public bool IsSequential => WorkerCount == 1; + + /// + public bool IsRunning { get; private set; } + + /// + public int WorkerCount { get; } + + /// + public IReadOnlyCollection ExecutedSteps => _executedSteps.ToArray(); + + /// + public bool IsCancelled { get; private set; } + + /// + /// Gets the logger instance used for logging messages related to the execution of the step runner. + /// + protected ILogger? Logger { get; } + + /// + /// Initializes a new instance of the class with the specified number of workers and a service provider. + /// + /// The number of workers to use for executing steps. Must be between 1 and 64, inclusive. + /// The service provider used to resolve dependencies required by the runner. + /// Thrown when is less than 1 or greater than 64. + /// Thrown when is . + public AsyncStepRunner(int workerCount, IServiceProvider serviceProvider) + { + if (workerCount is < 1 or > 64) + throw new ArgumentOutOfRangeException(nameof(workerCount)); + if (serviceProvider == null) + throw new ArgumentNullException(nameof(serviceProvider)); + Logger = serviceProvider.GetService()?.CreateLogger(GetType()); + WorkerCount = workerCount; + } + + /// + public virtual void AddStep(IStep step) + { + if (step == null) + throw new ArgumentNullException(nameof(step)); + _pendingSteps.Enqueue(step); + } + + /// + public Task RunAsync(CancellationToken token) + { + if (IsRunning) + throw new InvalidOperationException("The step runner is already running."); + + var task = CreateRunnerTask(token); + _completionSource.TrySetResult(task); + return task; + } + + /// + public TaskAwaiter GetAwaiter() + { + var task = _completionSource.Task; + return task.IsCompleted + ? task.Result.GetAwaiter() + : GetAwaitableTask().GetAwaiter(); + } + + private async Task GetAwaitableTask() + { + var task = await _completionSource.Task.ConfigureAwait(false); + await task.ConfigureAwait(false); + } + + /// + public void Wait() + { + Wait(Timeout.InfiniteTimeSpan); + } + + /// + public void Wait(TimeSpan timeout) + { + var tcsTask = _completionSource.Task; + if (!tcsTask.Wait(timeout)) + throw new TimeoutException(); + + var task = tcsTask.Result; + + var completed = true; + try + { + completed = task.Wait(timeout); + } + catch + { + // Ignore + } + + if (!completed) + throw new TimeoutException(); + + var exception = Exception; + if (exception != null) + throw exception; + } + + /// + /// Asynchronously retrieves the next step to be executed from the pending steps queue. + /// + /// A to observe while waiting for the next step. + /// + /// A representing the asynchronous operation. + /// The result contains the next to be executed, or if no steps are available. + /// + /// + /// This method is designed to be overridden in derived classes to customize the behavior of step retrieval. + /// + protected virtual ValueTask TakeNextStepAsync(CancellationToken cancellationToken) + { + return new ValueTask(_pendingSteps.TryDequeue(out var step) ? step : null); + } + + /// + /// Allows an overriding class to handle step errors and raises the event. + /// + /// The exception that caused the error. + /// The event args to use. + protected virtual void OnError(Exception exception, StepRunnerErrorEventArgs stepError) + { + Error?.Invoke(this, stepError); + IsCancelled |= stepError.Cancel; + } + + /// + /// Throws an if the given token was requested for cancellation. + /// + /// The token to check for cancellation. + /// If the token was requested for cancellation. + protected void ThrowIfCancelled(CancellationToken token) + { + token.ThrowIfCancellationRequested(); + if (IsCancelled) + throw new OperationCanceledException(token); + } + + /// + /// Allows an overriding class to perform cleanup actions once the runner was requested to stop execution. + /// + protected virtual void OnRunnerStopped() + { + } + + private async Task CreateRunnerTask(CancellationToken token) + { + try + { + IsRunning = true; + if (WorkerCount == 1) + await Task.Run(() => RunWorkerAsync(token), CancellationToken.None).ConfigureAwait(false); + else + { + var workers = new Task[WorkerCount]; + for (var i = 0; i < WorkerCount; i++) + workers[i] = Task.Factory.StartNew( + () => RunWorkerAsync(token), + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default).Unwrap(); + await Task.WhenAll(workers).ConfigureAwait(false); + } + } + finally + { + IsRunning = false; + } + } + + private async Task RunWorkerAsync(CancellationToken token) + { + var alreadyCancelled = false; + try + { + while (await TakeNextStepAsync(token).ConfigureAwait(false) is { } step) + { + try + { + ThrowIfCancelled(token); + _executedSteps.Add(step); + await step.RunAsync(token).ConfigureAwait(false); + } + catch (StopRunnerException e) + { + _exceptions.Add(e); + Logger?.LogTrace("Stop subsequent steps"); + IsCancelled = true; + + var error = new StepRunnerErrorEventArgs(e, step) + { + Cancel = true + }; + OnError(e, error); + + OnRunnerStopped(); + + break; + } + catch (Exception e) + { + _exceptions.Add(e); + if (!alreadyCancelled) + { + if (e.IsExceptionType()) + Logger?.LogTrace("Step {Step} cancelled", step); + else + Logger?.LogTrace(e, "Step {Step} threw an exception: {Exception}: {EMessage}", step, e.GetType(), e.Message); + } + + var error = new StepRunnerErrorEventArgs(e, step) + { + Cancel = token.IsCancellationRequested || IsCancelled || e.IsExceptionType() + }; + if (error.Cancel) + alreadyCancelled = true; + OnError(e, error); + } + } + } + catch (OperationCanceledException e) + { + IsCancelled = true; + OnError(e, new StepRunnerErrorEventArgs(e, null) + { + Cancel = true + }); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelProducerConsumerStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ParallelProducerConsumerStepRunner.cs deleted file mode 100644 index 3d045d5..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelProducerConsumerStepRunner.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Threading; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; - -/// -/// Runner engine, which executes all queued _steps parallel. Steps may be queued while step execution has been started. -/// The execution can finish only if was called explicitly. -/// -public sealed class ParallelProducerConsumerStepRunner : ParallelStepRunnerBase -{ - private BlockingCollection StepQueue { get; } = new(); - - /// - /// Initializes a new instance of the class with the specified number of workers. - /// - /// The number of parallel workers. - /// The service provider. - /// If the number of workers is below 1. - /// is . - public ParallelProducerConsumerStepRunner(int workerCount, IServiceProvider serviceProvider) : base(workerCount, serviceProvider) - { - } - - /// - /// Signals this instance does not expect any more steps. - /// - public void Finish() - { - StepQueue.CompleteAdding(); - } - - /// - public override void AddStep(IStep step) - { - if (step is null) - throw new ArgumentNullException(nameof(step)); - StepQueue.Add(step, CancellationToken.None); - } - - /// - protected override bool TakeNextStep(CancellationToken cancellationToken, [NotNullWhen(true)] out IStep? step) - { - step = null; - if (StepQueue.IsCompleted) - return false; - - try - { - step = StepQueue.Take(cancellationToken); - return true; - } - catch (InvalidOperationException) - { - return false; - } - } - - /// - protected override void OnRunnerStopped() - { - Finish(); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunner.cs deleted file mode 100644 index 637fc80..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunner.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Threading; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; - -/// -/// Runner engine, which executes all queued steps parallel. -/// -public class ParallelStepRunner : ParallelStepRunnerBase -{ - private ConcurrentQueue StepQueue { get; } = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The number of parallel workers. - /// The service provider. - /// If the number of workers is below 1 or above 64. - /// is . - public ParallelStepRunner(int workerCount, IServiceProvider serviceProvider) : base(workerCount, serviceProvider) - { - } - - /// - public override void AddStep(IStep step) - { - if (step == null) - throw new ArgumentNullException(nameof(step)); - StepQueue.Enqueue(step); - } - - /// - protected override bool TakeNextStep(CancellationToken cancellationToken, [NotNullWhen(true)] out IStep? step) - { - return StepQueue.TryDequeue(out step); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunnerBase.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunnerBase.cs deleted file mode 100644 index aa662d5..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunnerBase.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; - -/// -/// Base class for an that allows parallel step execution on the thread pool. -/// -public abstract class ParallelStepRunnerBase : StepRunnerBase, IParallelStepRunner -{ - private readonly ConcurrentBag _exceptions; - private readonly Task[] _tasks; - - /// - /// Gets the number of parallel workers. - /// - public int WorkerCount { get; } - - /// - public AggregateException? Exception => _exceptions.Count > 0 ? new AggregateException(_exceptions) : null; - - /// - /// Initializes a new instance of the class with the specified number of workers. - /// - /// The number of parallel workers. - /// The service provider. - /// If the number of workers is below 1 or above 64. - /// is . - protected ParallelStepRunnerBase(int workerCount, IServiceProvider serviceProvider) : base(serviceProvider) - { - if (workerCount is < 1 or > 64) - throw new ArgumentOutOfRangeException(nameof(workerCount)); - WorkerCount = workerCount; - _exceptions = []; - _tasks = new Task[workerCount]; - } - - /// - public override Task RunAsync(CancellationToken token) - { - for (var index = 0; index < WorkerCount; ++index) - _tasks[index] = Task.Factory.StartNew(() => RunSteps(token), TaskCreationOptions.LongRunning); - return Task.WhenAll(_tasks); - } - - /// - public void Wait() - { - Wait(Timeout.InfiniteTimeSpan); - } - - /// - public void Wait(TimeSpan timeout) - { - if (!Task.WaitAll(_tasks, timeout)) - throw new TimeoutException(); - - var exception = Exception; - if (exception != null) - throw exception; - } - - /// - protected override void OnError(Exception exception, StepRunnerErrorEventArgs stepError) - { - _exceptions.Add(exception); - base.OnError(exception, stepError); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs new file mode 100644 index 0000000..94ad35a --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs @@ -0,0 +1,124 @@ +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; + +/// +/// Represents a step runner that processes steps using a producer-consumer pattern. +/// +/// +/// +/// This class allows steps to be added dynamically while the runner is executing. +/// +/// +/// Call to signal completion of step additions, +/// otherwise the runner will block indefinitely unless cancelled via . +/// +/// +public class ProducerConsumerStepRunner(int workerCount, IServiceProvider serviceProvider) + : AsyncStepRunner(workerCount, serviceProvider) +{ + private readonly Channel _stepChannel = Channel.CreateUnbounded(); + + /// + /// Adds a step to the runner for execution. + /// + /// The step to add to the runner. + /// Thrown when the is . + /// Thrown when the runner has already been finished and cannot accept new steps. + public override void AddStep(IStep step) + { + if (!TryAddStep(step)) + throw new InvalidOperationException("Runner has been finished."); + } + + /// + /// Attempts to add a step to the runner for execution. + /// + /// The step to add to the runner. + /// + /// if the step was successfully added; otherwise, . + /// + /// Thrown when the is . + public bool TryAddStep(IStep step) + { + if (step == null) + throw new ArgumentNullException(nameof(step)); + return _stepChannel.Writer.TryWrite(step); + } + + /// + /// Signals this instance does not expect any more steps. + /// + /// + /// + /// This method must be called to allow to complete normally. + /// After calling this method, attempting to add more steps via + /// will throw an . + /// + /// + /// It is safe to call this method before, during, or after execution. + /// + /// + public void Finish() + { + _stepChannel.Writer.TryComplete(); + } + + /// + /// Asynchronously retrieves the next step to be executed from the internal queue. + /// + /// A token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous operation. The task result contains the next to be executed, + /// or if no more steps are available. + /// + /// + /// This method waits for a step to become available in the queue. If the queue is empty and no more steps will be added, + /// it returns . The operation can be cancelled by the provided . + /// + /// The operation is cancelled via the . + protected override async ValueTask TakeNextStepAsync(CancellationToken cancellationToken) + { + while (await _stepChannel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + if (_stepChannel.Reader.TryRead(out var step)) + return step; + } + return null; + } + + /// + /// Handles errors that occur during the execution of a step in the producer-consumer step runner. + /// + /// The exception that was thrown during the execution of the step. + /// + /// The containing details about the error, including the step + /// that caused the error and whether the runner should cancel further processing. + /// + /// + /// This method is invoked when an error occurs during the execution of a step. If the + /// property is set to true, the runner will terminate further processing by calling . + /// + protected override void OnError(Exception exception, StepRunnerErrorEventArgs stepError) + { + base.OnError(exception, stepError); + if (stepError.Cancel) + Finish(); + } + + /// + /// Performs cleanup actions when the runner is requested to stop execution. + /// + /// + /// This method overrides the base implementation to ensure that the runner + /// completes its processing by signaling that no more steps are expected. + /// + protected override void OnRunnerStopped() + { + base.OnRunnerStopped(); + Finish(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/SequentialStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/SequentialStepRunner.cs new file mode 100644 index 0000000..0ea4e8e --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/src/Runners/SequentialStepRunner.cs @@ -0,0 +1,8 @@ +using System; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; + +/// +/// A that executes steps sequentially using a single worker. +/// +public class SequentialStepRunner(IServiceProvider serviceProvider) : AsyncStepRunner(1, serviceProvider); \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/StepRunner.cs deleted file mode 100644 index e04e368..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunner.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; - -/// -/// Runner engine, which executes all queued sequentially in the order they are queued. -/// -public sealed class SequentialStepRunner : StepRunnerBase -{ - private ConcurrentQueue StepQueue { get; } = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The service provider for this instance. - /// is . - public SequentialStepRunner(IServiceProvider services) : base(services) - { - } - - /// - public override Task RunAsync(CancellationToken token) - { - return Task.Run(() => - { - RunSteps(token); - }, CancellationToken.None); - } - - /// - public override void AddStep(IStep step) - { - if (step == null) - throw new ArgumentNullException(nameof(step)); - StepQueue.Enqueue(step); - } - - /// - protected override bool TakeNextStep(CancellationToken cancellationToken, [NotNullWhen(true)] out IStep? step) - { - return StepQueue.TryDequeue(out step); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs b/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs deleted file mode 100644 index 2db7260..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; - -/// -/// Base class for an . -/// -public abstract class StepRunnerBase : IStepRunner -{ - /// - public event EventHandler? Error; - - /// - /// Gets a modifiable bag of all executed steps. - /// - protected readonly ConcurrentBag ExecutedStepsBag = []; - - /// - /// Gets the logger instance of this stepRunner. - /// - protected ILogger? Logger { get; } - - /// - public IReadOnlyCollection ExecutedSteps => ExecutedStepsBag.ToArray(); - - internal bool IsCancelled { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// The service provider for this instance. - /// is . - protected StepRunnerBase(IServiceProvider services) - { - if (services == null) - throw new ArgumentNullException(nameof(services)); - Logger = services.GetService()?.CreateLogger(GetType()); - } - - /// - public abstract Task RunAsync(CancellationToken token); - - /// - public abstract void AddStep(IStep step); - - /// - /// Tries to get the next step from the step queue. - /// - /// The cancellation token - /// When this method returns, contains the next step to execute, or if there is no step to execute. - /// when there exists a next step; otherwise, . - protected abstract bool TakeNextStep(CancellationToken cancellationToken, [NotNullWhen(true)] out IStep? step); - - /// - /// Takes steps from the step queue and executes it. - /// - /// The cancellation token. - protected void RunSteps(CancellationToken token) - { - var alreadyCancelled = false; - try - { - while (TakeNextStep(token, out var step)) - { - try - { - ThrowIfCancelled(token); - - ExecutedStepsBag.Add(step); - step.Run(token); - } - catch (StopRunnerException) - { - OnRunnerStopped(); - Logger?.LogTrace("Stop subsequent steps"); - break; - } - catch (Exception e) - { - if (!alreadyCancelled) - { - if (e.IsExceptionType()) - Logger?.LogTrace("Step {Step} cancelled", step); - else - Logger?.LogTrace(e, "Step {Step} threw an exception: {Exception}: {EMessage}", step, e.GetType(), e.Message); - } - - var error = new StepRunnerErrorEventArgs(e, step) - { - Cancel = token.IsCancellationRequested || IsCancelled || e.IsExceptionType() - }; - if (error.Cancel) - alreadyCancelled = true; - OnError(e, error); - } - } - } - catch (OperationCanceledException e) - { - OnError(e, new StepRunnerErrorEventArgs(e, null)); - IsCancelled = true; - } - } - - /// - /// Allows an overriding class to handle step errors and raises the event. - /// - /// The exception that caused the error. - /// The event args to use. - protected virtual void OnError(Exception exception, StepRunnerErrorEventArgs stepError) - { - Error?.Invoke(this, stepError); - if (!stepError.Cancel) - return; - IsCancelled |= stepError.Cancel; - } - - /// - /// Throws an if the given token was requested for cancellation. - /// - /// The token to check for cancellation. - /// If the token was requested for cancellation. - protected void ThrowIfCancelled(CancellationToken token) - { - token.ThrowIfCancellationRequested(); - if (IsCancelled) - throw new OperationCanceledException(token); - } - - /// - /// Allows an overriding class to perform cleanup actions once the runner was requested to stop execution. - /// - protected virtual void OnRunnerStopped() - { - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/StepFailureException.cs b/src/CommonUtilities.SimplePipeline/src/StepFailureException.cs index f5ef830..5955fd5 100644 --- a/src/CommonUtilities.SimplePipeline/src/StepFailureException.cs +++ b/src/CommonUtilities.SimplePipeline/src/StepFailureException.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text; namespace AnakinRaW.CommonUtilities.SimplePipeline; @@ -10,7 +11,13 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline; /// public sealed class StepFailureException : Exception { - private readonly IEnumerable _failedSteps; + /// + /// Gets the collection of steps that failed. + /// + /// + /// A read-only collection of instances representing the failed steps. + /// + public IReadOnlyCollection FailedSteps { get; } /// public override string Message => Error; @@ -22,11 +29,14 @@ private string Error { if (field is not null) return field; - - var stringBuilder = new StringBuilder(); - - foreach (var step in _failedSteps) - stringBuilder.Append($"Step '{step}' failed with error: {step.Error?.Message};"); + var stringBuilder = new StringBuilder($"{FailedSteps.Count} Failed Step(s)"); + if (FailedSteps.Count > 0) + { + stringBuilder.Append(':'); + stringBuilder.Append(' '); + } + foreach (var step in FailedSteps) + stringBuilder.Append($"Step '{step}' failed with error: {step.Error?.Message ?? "n/a"};"); field = stringBuilder.ToString().TrimEnd(';'); return field; } @@ -38,6 +48,8 @@ private string Error /// The failed steps. public StepFailureException(IEnumerable failedSteps) { - _failedSteps = failedSteps ?? throw new ArgumentNullException(nameof(failedSteps)); + if (failedSteps == null) + throw new ArgumentNullException(nameof(failedSteps)); + FailedSteps = failedSteps.ToList(); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/AsyncStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/AsyncStep.cs deleted file mode 100644 index a14b6de..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Steps/AsyncStep.cs +++ /dev/null @@ -1,86 +0,0 @@ -//using Microsoft.Extensions.DependencyInjection; -//using Microsoft.Extensions.Logging; -//using System; -//using System.Runtime.CompilerServices; -//using System.Threading; -//using System.Threading.Tasks; - -//namespace AnakinRaW.CommonUtilities.StepRunnerPipeline.Steps; - -///// -///// A that can be awaited on. -///// -//public abstract class AsyncStep : DisposableObject, IStep -//{ -// private readonly TaskCompletionSource _taskCompletion = new(); - -// /// -// /// Gets the service provider of this step. -// /// -// protected IServiceProvider Services { get; } - -// /// -// /// Gets the logger of this step. -// /// -// protected ILogger? Logger { get; } - -// /// -// public Exception? Error -// { -// get -// { -// if (_taskCompletion.Task.IsFaulted) -// return _taskCompletion.Task.Exception?.InnerException; - -// if (!_taskCompletion.Task.IsCompleted) -// return null; - -// if (_taskCompletion.Task.Result.IsFaulted) -// return _taskCompletion.Task.Result.Exception?.InnerException; - -// return null; -// } -// } - -// /// -// /// Initializes a new instance of the class. -// /// -// /// The service provider. -// /// is . -// protected AsyncStep(IServiceProvider serviceProvider) -// { -// Services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); -// Logger = serviceProvider.GetService()?.CreateLogger(GetType()); -// } - -// /// -// /// Gets an awaiter used to await this . -// /// -// /// An awaiter instance. -// public TaskAwaiter GetAwaiter() -// { -// if (_taskCompletion.Task.IsCompleted) -// return _taskCompletion.Task.Result.GetAwaiter(); - -// return Task.Run(async () => -// { -// var task = await _taskCompletion.Task.ConfigureAwait(false); -// await task.ConfigureAwait(false); -// }).GetAwaiter(); -// } - -// /// -// /// Run the step's action and returns the operation as a task reference. -// /// -// /// -// /// The task that represents the operation of this step. -// protected abstract Task RunAsync(CancellationToken token); - -// /// -// public void Run(CancellationToken token) -// { -// Logger?.LogTrace($"BEGIN on thread-pool: {this}"); -// var task = RunAsync(token); -// _taskCompletion.SetResult(task); -// } -//} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs index 94f4c41..6726ec5 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs @@ -1,8 +1,10 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using System.Threading.Tasks; namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps; @@ -11,6 +13,8 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps; /// public abstract class PipelineStep : DisposableObject, IStep { + private readonly TaskCompletionSource _completionSource = new(); + /// /// Returns the service provider of this step. /// @@ -22,8 +26,17 @@ public abstract class PipelineStep : DisposableObject, IStep protected readonly ILogger? Logger; /// - /// Gets the exception that occurred during execution or if no error occurred. + /// Gets the exception that occurred during the execution of the step, + /// or if the step completed successfully. /// + /// + /// If the step is cancelled by an + /// (which may also be wrapped inside an ), + /// this property contains the underlying cause of the cancellation when available, + /// otherwise it may be . + /// For all other failures, this property contains the exception that caused + /// the step to fail. + /// public Exception? Error { get; internal set; } /// @@ -36,14 +49,53 @@ protected PipelineStep(IServiceProvider serviceProvider) Services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); Logger = serviceProvider.GetService()?.CreateLogger(GetType()); } - + /// - public void Run(CancellationToken token) + public Task RunAsync(CancellationToken token) + { + var task = ExecuteStepAsync(token); + _completionSource.TrySetResult(task); + return task; + } + + /// + public TaskAwaiter GetAwaiter() + { + var tcsTask = _completionSource.Task; + return tcsTask is { IsCompleted: true, Status: TaskStatus.RanToCompletion } + ? tcsTask.Result.GetAwaiter() + : GetAwaitableTask().GetAwaiter(); + } + + /// + /// Returns a string that represents the current instance. + /// + /// + /// A string that represents the current instance, typically the name of the step's type. + /// + public override string ToString() + { + return GetType().Name; + } + + /// 0 + /// Executes this step. + /// + /// Provided to allow cancellation. + protected abstract Task RunCoreAsync(CancellationToken token); + + private async Task GetAwaitableTask() + { + var task = await _completionSource.Task.ConfigureAwait(false); + await task.ConfigureAwait(false); + } + + private async Task ExecuteStepAsync(CancellationToken token) { Logger?.LogTrace("BEGIN: {Step}", this); try { - RunCore(token); + await RunCoreAsync(token).ConfigureAwait(false); Logger?.LogTrace("END: {Step}", this); } catch (OperationCanceledException ex) @@ -55,36 +107,27 @@ public void Run(CancellationToken token) { throw; } - catch (AggregateException ex) + catch (AggregateException e) { - if (!ex.IsExceptionType()) - LogFaultException(ex); + if (!e.IsExceptionType()) + { + Error = e; + LogFaultException(e); + } else - Error = ex.InnerExceptions.FirstOrDefault(p => p.IsExceptionType())?.InnerException; + Error = e.InnerExceptions.FirstOrDefault(p => p.IsExceptionType())?.InnerException; throw; } catch (Exception e) { + Error = e; LogFaultException(e); throw; } } - /// - public override string ToString() - { - return GetType().Name; - } - - /// - /// Executes this step. - /// - /// Provided to allow cancellation. - protected abstract void RunCore(CancellationToken token); - private void LogFaultException(Exception ex) { - Error = ex; Logger?.LogError(ex, ex.InnerException?.Message ?? ex.Message); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs index 7a13f29..85f243a 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs @@ -1,34 +1,55 @@ using System; -using System.Linq; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps; /// -/// A step that executes a pipeline and waits for the pipeline to end. +/// Represents a pipeline step that executes a specific . /// -/// The pipeline to execute. -/// The service provider -public class RunPipelineStep(IPipeline pipeline, IServiceProvider serviceProvider) : SynchronizedStep(serviceProvider) +/// +/// This step is responsible for running the provided pipeline and managing its lifecycle, +/// including handling exceptions and disposing of resources. +/// +public sealed class RunPipelineStep : PipelineStep { - private readonly IPipeline _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); + private readonly IPipeline _pipeline; - /// - protected override void RunSynchronized(CancellationToken token) + /// + /// Initializes a new instance of the class. + /// + /// The pipeline to be executed by this step. + /// The service provider used to resolve dependencies. + /// + /// Thrown when or is . + /// + public RunPipelineStep(IPipeline pipeline, IServiceProvider serviceProvider) : base(serviceProvider) + { + _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); + } + + /// + /// Executes the core logic of the pipeline step asynchronously. + /// + /// + /// This method is responsible for running the associated and handling its lifecycle. + /// It logs the start and completion of the pipeline execution, and captures any exceptions that occur during execution. + /// + /// A to observe while waiting for the task to complete. + /// A representing the asynchronous operation. + /// The pipeline execution encounters an error. + protected override async Task RunCoreAsync(CancellationToken token) { Logger?.LogTrace("Running {Pipeline}...", _pipeline); try { - // ReSharper disable once MethodSupportsCancellation - _pipeline.RunAsync(token).Wait(); + await _pipeline.RunAsync(token).ConfigureAwait(false); Logger?.LogTrace("Finished {Pipeline}", _pipeline); } - catch (AggregateException e) + catch (Exception e) { - var root = e.InnerExceptions.FirstOrDefault(); - if (root is not null) - throw root; + Logger?.LogError(e, "Pipeline {Pipeline} finished with exception: {Message}", _pipeline, e.Message); throw; } } diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/SynchronizedStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/SynchronizedStep.cs deleted file mode 100644 index 618c1ba..0000000 --- a/src/CommonUtilities.SimplePipeline/src/Steps/SynchronizedStep.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Threading; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps; - -/// -/// A step that can be waited for. -/// -public abstract class SynchronizedStep : PipelineStep -{ - /// - /// Event gets raised if this instance failed with an . - /// - public event EventHandler? Canceled; - - private readonly ManualResetEvent _handle; - - /// - /// Initializes a new instance of the class. - /// - /// The service provider. - /// is . - protected SynchronizedStep(IServiceProvider serviceProvider) : base(serviceProvider) - { - _handle = new ManualResetEvent(false); - } - - /// - /// Waits until the predefined stepRunner has finished. - /// - public void Wait() - { - Wait(Timeout.InfiniteTimeSpan); - } - - /// - /// Waits until the predefined stepRunner has finished. - /// - /// The time duration to wait. - /// If . - public void Wait(TimeSpan timeout) - { - if (!_handle.WaitOne(timeout)) - throw new TimeoutException(); - } - - /// - /// Executes this step. - /// - /// - protected abstract void RunSynchronized(CancellationToken token); - - /// - protected override void DisposeResources() - { - base.DisposeResources(); - _handle.Dispose(); - } - - /// - protected sealed override void RunCore(CancellationToken token) - { - try - { - RunSynchronized(token); - } - catch (Exception ex) - { - if (ex.IsExceptionType()) - Canceled?.Invoke(this, EventArgs.Empty); - throw; - } - finally - { - _handle.Set(); - } - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs index d8a5616..52bbb39 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs @@ -1,16 +1,17 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps; /// -/// A step that waits for a given to finish. +/// A step that waits for a given to finish. /// public sealed class WaitStep : PipelineStep { - private readonly IParallelStepRunner _stepRunner; + private readonly IStepRunner _stepRunner; /// /// Initializes a new instance of the class with the specified stepRunner. @@ -18,7 +19,7 @@ public sealed class WaitStep : PipelineStep /// The step runner. /// The service provider. /// or is . - public WaitStep(IParallelStepRunner stepRunner, IServiceProvider serviceProvider) : base(serviceProvider) + public WaitStep(IStepRunner stepRunner, IServiceProvider serviceProvider) : base(serviceProvider) { _stepRunner = stepRunner ?? throw new ArgumentNullException(nameof(stepRunner)); } @@ -32,15 +33,12 @@ public WaitStep(IParallelStepRunner stepRunner, IServiceProvider serviceProvider /// /// Provided to allow cancellation. /// If awaiting the stepRunner failed with an exception. - protected override void RunCore(CancellationToken token) + protected override async Task RunCoreAsync(CancellationToken token) { - try + await _stepRunner; + if (_stepRunner.Exception is not null) { - _stepRunner.Wait(); - } - catch - { - Logger?.LogTrace("Wait step is stopping all subsequent steps"); + Logger?.LogTrace("The awaited step runner has exceptions. Stopping all subsequent steps."); throw new StopRunnerException(); } } diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index 5f8d439..fed10fe 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -14,11 +14,11 @@ - + - + - + @@ -38,4 +38,8 @@ + + + + diff --git a/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs b/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs index ba25f27..b6810a3 100644 --- a/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs @@ -1,26 +1,119 @@ -using System; +using AnakinRaW.CommonUtilities.Testing; +using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.Testing.Extensions; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test; -public class ExtensionsTest +public class ExtensionsTest : TestBaseWithServiceProvider { [Fact] public void IsExceptionType() { - var e = new InvalidOperationException(); + var i = new InvalidOperationException(); - Assert.True(e.IsExceptionType()); - Assert.False(e.IsExceptionType()); + Assert.True(i.IsExceptionType()); + Assert.False(i.IsExceptionType()); var io = new IOException(); Assert.True(io.IsExceptionType()); Assert.True(io.IsExceptionType()); - var a = new AggregateException(new List { e, io }); + var a = new AggregateException(new List { i, io }); + Assert.True(a.IsExceptionType()); Assert.True(a.IsExceptionType()); Assert.True(a.IsExceptionType()); + + var n = new NullReferenceException(); + var da = new AggregateException(a, n); + Assert.True(da.IsExceptionType()); + Assert.True(da.IsExceptionType()); + Assert.True(da.IsExceptionType()); + Assert.True(da.IsExceptionType()); + } + + [Fact] + public void ThrowStepFailureExceptionForFailedSteps_DoesNotThrowIfEmpty() + { + IEnumerable steps = []; + var exception = Record.Exception(() => steps.ThrowStepFailureExceptionForFailedSteps()); + Assert.Null(exception); + } + + [Fact] + public void ThrowStepFailureExceptionForFailedSteps_DoesNotThrowIfNoStepFailed() + { + var step1 = new ErrorStep { Error = null }; + var step2 = new ErrorStep { Error = new OperationCanceledException() }; + var step3 = new ErrorStep { Error = new AggregateException(new OperationCanceledException()) }; + var step4 = new ErrorStep { Error = new AggregateException(new Exception(), new OperationCanceledException()) }; + + IEnumerable steps = [step1, step2, step3, step4]; + var exception = Record.Exception(() => steps.ThrowStepFailureExceptionForFailedSteps()); + Assert.Null(exception); + } + + private static IEnumerable GetNonFailedSteps() + { + yield return new ErrorStep { Error = null }; + yield return new ErrorStep { Error = new OperationCanceledException() }; + yield return new ErrorStep { Error = new TaskCanceledException() }; + yield return new ErrorStep { Error = new AggregateException(new OperationCanceledException()) }; + yield return new ErrorStep { Error = new AggregateException(new TaskCanceledException()) }; + yield return new ErrorStep { Error = new AggregateException(new AggregateException(new OperationCanceledException())) }; + yield return new ErrorStep { Error = new AggregateException(new Exception(), new OperationCanceledException()) }; + } + + private static IEnumerable GetFailedSteps() + { + yield return new ErrorStep { Error = new Exception() }; + yield return new ErrorStep { Error = new AggregateException(new ArgumentException()) }; + } + + [Fact] + public void ThrowStepFailureExceptionForFailedSteps_ThrowsIfOneStepFailed() + { + var exception = Assert.Throws(() => + GetNonFailedSteps().Append(GetFailedSteps().First()) + .ThrowStepFailureExceptionForFailedSteps()); + Assert.Single(exception.FailedSteps); + } + + [Fact] + public void ThrowStepFailureExceptionForFailedSteps_ThrowsIfAllStepsFailed() + { + var failed = GetFailedSteps().ToList(); + var exception = Assert.Throws(() => failed.ThrowStepFailureExceptionForFailedSteps()); + Assert.EqualUnordered(failed, exception.FailedSteps.ToList()); + } + + [Fact] + public void ThrowStepFailureExceptionForFailedSteps_ThrowsIfManyStepsFailed() + { + var failed = GetFailedSteps().ToList(); + var exception = Assert.Throws(() => failed.Concat(GetNonFailedSteps()).ThrowStepFailureExceptionForFailedSteps()); + Assert.All(exception.FailedSteps, step => + { + Assert.Contains(step, failed); + }); + } + + private class ErrorStep : IStep + { + public Exception? Error { get; init; } + + public void Dispose() + { + } + + public Task RunAsync(CancellationToken token) => throw new NotImplementedException(); + + public TaskAwaiter GetAwaiter() => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs index 744dd01..3f67e3c 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs @@ -1,29 +1,67 @@ -using System; +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; +using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -// ReSharper disable once UnusedMember.Global -public class ParallelPipelineTests : StepRunnerPipelineTest +public class ParallelPipelineTests : StepRunnerPipelineTestBase { - protected override Pipeline CreatePipeline(IList steps) + protected override ITrackingPipeline CreateTrackingPipeline(Func prepare, Func run) { - return CreatePipeline(steps, true); + var testStep = new TestStep(run, ServiceProvider); + return new TestParallelPipeline(ServiceProvider, [testStep], prepare, GetWorkerCount(GetRandomRunBehavior()), false); } - protected override StepRunnerPipeline CreatePipeline(IList steps, bool failFast) + protected override StepRunnerPipeline CreateStepRunnerPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior) { - return new TestParallelPipeline(steps, ServiceProvider, failFast: failFast); + return new TestParallelPipeline(ServiceProvider, steps, null, GetWorkerCount(runnerBehavior), failFast); } - - private class TestParallelPipeline(IList steps, IServiceProvider serviceProvider, int workerCount = 4, bool failFast = true) - : ParallelPipeline(serviceProvider, workerCount, failFast) + + #region Constructor Tests + + [Fact] + public void Ctor_NullServiceProvider_Throws() { - protected override Task> BuildSteps() + Assert.Throws(() => new TestParallelPipeline(null!, [], null)); + } + + #endregion + + private class TestParallelPipeline : StepRunnerPipeline, ITrackingPipeline + { + private readonly IList _steps; + private readonly int _workerCount; + private readonly Func? _prepareAction; + + public TestParallelPipeline( + IServiceProvider serviceProvider, + IEnumerable steps, + Func? onPrepare, + int workerCount = 4, + bool failFast = true) + : base(serviceProvider) + { + _steps = steps.ToList(); + _workerCount = workerCount; + FailFast = failFast; + _prepareAction = onPrepare; + } + + protected override IStepRunner CreateRunner() + { + return new AsyncStepRunner(_workerCount, ServiceProvider); + } + + protected override async Task> CreateRunnerSteps(CancellationToken token) { - return Task.FromResult(steps); + if (_prepareAction is not null) + await _prepareAction(token); + return _steps; } } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs deleted file mode 100644 index 2dfeff4..0000000 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelProducerConsumerPipelineTest.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; - -public class ParallelProducerConsumerPipelineTest : PipelineTest -{ - [Fact] - public async Task RunAsync_DelayedAdd() - { - var tcs = new TaskCompletionSource(); - - var s1 = new TestStep(_ => - { - Task.Delay(3000, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); - tcs.SetResult(0); - }, ServiceProvider); - - var s2Run = false; - var s2 = new TestStep(_ => - { - s2Run = true; - }, ServiceProvider); - - var pipeline = CreateConsumerPipeline(ValueFunction()); - - await pipeline.RunAsync(TestContext.Current.CancellationToken); - - Assert.True(s2Run); - - return; - - async IAsyncEnumerable ValueFunction() - { - yield return s1; - await tcs.Task; - yield return s2; - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task RunAsync_DelayedAdd_PrepareFails(bool failFast) - { - var mre = new ManualResetEventSlim(false); - - var ran = false; - var s1 = new TestStep(ct => - { - mre.Wait(ct); - ct.ThrowIfCancellationRequested(); - ran = true; - - }, ServiceProvider); - var s2 = new TestStep(_ => { }, ServiceProvider); - - var pipeline = CreateConsumerPipeline(ValueFunction(), failFast); - - var task = Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); - - if (failFast) - await task; - - mre.Set(); - - await task; - - if (failFast) - Assert.False(ran); - else - Assert.True(ran); - - return; - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - async IAsyncEnumerable ValueFunction() -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - yield return s1; - yield return s2; - throw new ApplicationException("test"); - } - } - - [Fact] - public async Task PrepareAsync_PrepareFails() - { - var s1 = new TestStep(_ => { }, ServiceProvider); - var s2 = new TestStep(_ => { }, ServiceProvider); - - var pipeline = CreateConsumerPipeline(ValueFunction()); - - await Assert.ThrowsAsync(pipeline.PrepareAsync); - - return; - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - async IAsyncEnumerable ValueFunction() -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - yield return s1; - yield return s2; - throw new ApplicationException("test"); - } - } - - - [Fact] - public async Task RunAsync_PrepareCancelled() - { - var cts = new CancellationTokenSource(); - var tcs = new TaskCompletionSource(); - - var s1 = new TestStep(_ => - { - Task.Delay(3000, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); - tcs.SetResult(0); - }, ServiceProvider); - - var s2Run = false; - var s2 = new TestStep(_ => - { - s2Run = true; - }, ServiceProvider); - - var pipeline = CreateConsumerPipeline(ValueFunction()); - - await Assert.ThrowsAsync(async () => await pipeline.RunAsync(cts.Token)); - - Assert.False(s2Run); - - return; - - async IAsyncEnumerable ValueFunction() - { - yield return s1; - await tcs.Task; - cts.Cancel(); - await Task.Delay(1000, TestContext.Current.CancellationToken); - yield return s2; - } - } - - protected override Pipeline CreatePipeline(IList steps) - { - return new TestParallelProducerConsumerPipeline(steps.ToAsyncEnumerable(), 4, true, ServiceProvider); - } - - private Pipeline CreateConsumerPipeline(IAsyncEnumerable steps, bool failFast = true) - { - return new TestParallelProducerConsumerPipeline(steps, 4, failFast, ServiceProvider); - } - - private class TestParallelProducerConsumerPipeline( - IAsyncEnumerable steps, - int workerCount, - bool failFast, - IServiceProvider serviceProvider) - : ParallelProducerConsumerPipeline(workerCount, failFast, serviceProvider) - { - protected override IAsyncEnumerable BuildSteps() - { - return steps; - } - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs index 2458253..890efe6 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs @@ -2,168 +2,45 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.Testing; -using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -public abstract class PipelineTest : TestBaseWithServiceProvider +public class PipelineTest : PipelineTestBase { - protected abstract Pipeline CreatePipeline(IList steps); - - [Fact] - public async Task Prepare() + protected override Pipeline CreatePipeline(IList steps) { - var s = new TestStep(_ => { }, ServiceProvider); - var pipeline = CreatePipeline([s]); - - await pipeline.PrepareAsync(); - await pipeline.PrepareAsync(); + return new TestPipeline( + _ => Task.CompletedTask, + async ct => + { + foreach (var step in steps) + { + await step.RunAsync(ct); + } + }, + ServiceProvider); } - [Fact] - public async Task Dispose() + protected override ITrackingPipeline CreateTrackingPipeline( + Func prepare, + Func run) { - var pipeline = CreatePipeline([]); - - pipeline.Dispose(); - - Assert.True(pipeline.IsDisposed); - await Assert.ThrowsAsync(pipeline.PrepareAsync); - await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); + return new TestPipeline(prepare, run, ServiceProvider); } - [Fact] - public async Task Run_RunMultipleTimesDoesNotPrepareAgain_StepRunOnlyOnce() + private class TestPipeline( + Func prepare, + Func run, + IServiceProvider serviceProvider) : Pipeline(serviceProvider), ITrackingPipeline { - var counter = 0; - var s = new TestStep(_ => { counter++; }, ServiceProvider); - var pipeline = CreatePipeline([s]); - - await pipeline.RunAsync(TestContext.Current.CancellationToken); - await pipeline.RunAsync(TestContext.Current.CancellationToken); - - Assert.Equal(1, counter); - } - - [Fact] - public async Task PrepareThenRun() - { - var counter = 0; - var s = new TestStep(_ => { counter++; }, ServiceProvider); - var pipeline = CreatePipeline([s]); - - await pipeline.PrepareAsync(); - await pipeline.RunAsync(TestContext.Current.CancellationToken); - Assert.Equal(1, counter); - } - - [Fact] - public async Task Run_Cancelled_ThrowsOperationCanceledException() - { - var counter = 0; - var s = new TestStep(_ => { counter++; }, ServiceProvider); - var pipeline = CreatePipeline([s]); - - var cts = new CancellationTokenSource(); - cts.Cancel(); - await Assert.ThrowsAsync(async () => await pipeline.RunAsync(cts.Token)); - Assert.Equal(0, counter); - } - - [Fact] - public async Task Prepare_Disposed_ThrowsObjectDisposedException() - { - var counter = 0; - var s = new TestStep(_ => { counter++; }, ServiceProvider); - var pipeline = CreatePipeline([s]); - - pipeline.Dispose(); - pipeline.Dispose(); - - await Assert.ThrowsAsync(pipeline.PrepareAsync); - await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); - - Assert.Equal(0, counter); - Assert.False(pipeline.PipelineFailed); - } - - [Fact] - public async Task Run_Disposed_ThrowsObjectDisposedException() - { - var counter = 0; - var s = new TestStep(_ => { counter++; }, ServiceProvider); - var pipeline = CreatePipeline([s]); - - await pipeline.PrepareAsync(); - pipeline.Dispose(); - - await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); - Assert.Equal(0, counter); - Assert.False(pipeline.PipelineFailed); - } - - [Fact] - public async Task Cancel() - { - var waitToCancel = new TaskCompletionSource(); - var waitUntilCanceled = new ManualResetEvent(false); - - var token = CancellationToken.None; - - var step = new TestStep(ct => + protected override Task PrepareCoreAsync(CancellationToken token) { - token = ct; - waitToCancel.SetResult(0); - waitUntilCanceled.WaitOne(); - - }, ServiceProvider); - - var pipeline = CreatePipeline([step]); - - await pipeline.PrepareAsync(); - - var pipelineTask = pipeline.RunAsync(CancellationToken.None); - await waitToCancel.Task; - pipeline.Cancel(); - waitUntilCanceled.Set(); - - await Assert.ThrowsAsync(async () => await pipelineTask); - - Assert.True(pipeline.PipelineFailed); - - Assert.True(token.IsCancellationRequested); - } + return prepare(token); + } - [Fact] - public async Task Cancel_BeforeRun_HasNoEffect() - { - var ran = false; - var step = new TestStep(_ => ran = true, ServiceProvider); - - var pipeline = CreatePipeline([step]); - - await pipeline.PrepareAsync(); - pipeline.Cancel(); - - await pipeline.RunAsync(CancellationToken.None); - Assert.False(pipeline.PipelineFailed); - Assert.True(ran); - } - - [Fact] - public async Task RunAsync_TokenCancelledBeforeRun() - { - var ran = false; - var step = new TestStep(_ => ran = true, ServiceProvider); - - var pipeline = CreatePipeline([step]); - - await pipeline.PrepareAsync(); - - await Assert.ThrowsAsync(async () => await pipeline.RunAsync(new CancellationToken(true))); - - Assert.True(pipeline.PipelineFailed); - Assert.False(ran); + protected override Task ExecuteAsync(CancellationToken token) + { + return Task.Run(() => run(token), CancellationToken.None); + } } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestBase.cs new file mode 100644 index 0000000..79dc947 --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestBase.cs @@ -0,0 +1,728 @@ +using AnakinRaW.CommonUtilities.Testing; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; +using Xunit; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; + +public abstract class PipelineTestBase : TestBaseWithServiceProvider +{ + protected abstract Pipeline CreatePipeline(IList steps); + + protected abstract ITrackingPipeline CreateTrackingPipeline( + Func prepare, + Func run); + + protected virtual ITrackingPipeline CreateTrackingPipeline(Action prepare, Action run) + { + return CreateTrackingPipeline( + _ => Task.Run(prepare, CancellationToken.None), + _ => Task.Run(run, CancellationToken.None)); + } + + #region Initialization + + [Fact] + public void Ctor_WithValidServiceProvider_InitializesCorrectly() + { + var pipeline = CreatePipeline([]); + Assert.False(pipeline.Failed); + Assert.False(pipeline.Cancelled); + Assert.False(pipeline.IsDisposed); + Assert.False(pipeline.IsPrepared); + } + + #endregion + + #region PrepareAsync Tests + + [Fact] + public async Task PrepareAsync_CalledOnce_Succeeds() + { + var pipeline = CreatePipeline([]); + + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + + Assert.False(pipeline.Failed); + Assert.True(pipeline.IsPrepared); + } + + [Fact] + public async Task PrepareAsync_CalledMultipleTimes_ThrowsInvalidOperationException() + { + var prepareCount = 0; + var pipeline = CreateTrackingPipeline(() => prepareCount++, () => { }); + + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + await Assert.ThrowsAsync(async () => await pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + + Assert.Equal(1, prepareCount); + } + + [Fact] + public async Task PrepareAsync_ConcurrentCalls_OnlyFirstSucceeds() + { + var prepareCount = 0; + var barrier = new TaskCompletionSource(); + + var pipeline = CreateTrackingPipeline(async _ => + { + Interlocked.Increment(ref prepareCount); + await barrier.Task; + }, _ => Task.CompletedTask); + + var firstTask = pipeline.PrepareAsync(CancellationToken.None); + + await Task.Delay(50, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + pipeline.PrepareAsync(CancellationToken.None)); + + barrier.SetResult(true); + await firstTask; + + Assert.Equal(1, prepareCount); + } + + [Fact] + public async Task PrepareAsync_FailedPreparation_CannotRetry() + { + var callCount = 0; + var pipeline = CreateTrackingPipeline(_ => + { + callCount++; + throw new ArgumentException("Preparation failed"); + }, _ => Task.CompletedTask); + + await Assert.ThrowsAsync(() => + pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + + await Assert.ThrowsAsync(() => + pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + + Assert.Equal(1, callCount); + Assert.False(((Pipeline)pipeline).IsPrepared); + } + + [Fact] + public async Task PrepareAsync_Cancelled_ThrowsOperationCancelledException() + { + var mre = new ManualResetEventSlim(false); + var cts = new CancellationTokenSource(); + + var pipeline = CreateTrackingPipeline(async ct => + { + await Task.Yield(); + mre.Wait(TestContext.Current.CancellationToken); + ct.ThrowIfCancellationRequested(); + }, _ => Task.CompletedTask); + + var prepareTask = pipeline.PrepareAsync(cts.Token); + cts.Cancel(); + mre.Set(); + + await Assert.ThrowsAsync(async () => await prepareTask); + Assert.False(((Pipeline)pipeline).IsPrepared); + } + + [Fact] + public async Task PrepareAsync_Disposed_ThrowsObjectDisposedException() + { + var pipeline = CreatePipeline([]); + pipeline.Dispose(); + + await Assert.ThrowsAsync( + () => pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task PrepareAsync_WithCancelledToken_ThrowsOperationCanceledException() + { + var pipeline = CreatePipeline([]); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(() => pipeline.PrepareAsync(cts.Token)); + Assert.False(pipeline.IsPrepared); + } + + #endregion + + #region RunAsync Tests + + [Fact] + public async Task RunAsync_WithoutExplicitPrepare_PreparesAutomatically() + { + var prepareCount = 0; + var runCount = 0; + var pipeline = CreateTrackingPipeline(() => prepareCount++, () => runCount++); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.Equal(1, prepareCount); + Assert.Equal(1, runCount); + Assert.True(((Pipeline)pipeline).IsPrepared); + } + + [Fact] + public async Task RunAsync_PreparationThrowsNonCancellationException_SetsPipelineFailed() + { + var pipeline = CreateTrackingPipeline( + _ => throw new ArgumentException("Preparation failed"), + _ => Task.CompletedTask); + + await Assert.ThrowsAsync(() => + pipeline.RunAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task RunAsync_PreparationFailed_RethrowsPreparationException() + { + var pipeline = CreateTrackingPipeline( + _ => throw new ArgumentException("Preparation failed"), + _ => Task.CompletedTask); + + await Assert.ThrowsAsync(async () => await pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task RunAsync_PreparationThrowsOperationCanceledException_SetsPipelineCancelled() + { + var pipeline = CreateTrackingPipeline( + _ => throw new OperationCanceledException(), + _ => Task.CompletedTask); + + await Assert.ThrowsAsync(() => + pipeline.RunAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task RunAsync_CalledMultipleTimes_ThrowsInvalidOperationException() + { + var counter = 0; + var s = new TestStep(_ => + { + counter++; + return Task.CompletedTask; + }, ServiceProvider); + var pipeline = CreatePipeline([s]); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); + + // RunAsync also prepares the pipeline, thus this throws too + await Assert.ThrowsAsync(async () => await pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + + Assert.Equal(1, counter); + } + + [Fact] + public async Task RunAsync_CalledWhilePrepareAsyncInProgress_WaitsForSamePreparation() + { + var preparationStarted = new TaskCompletionSource(); + var continuePreparation = new TaskCompletionSource(); + var executed = false; + var prepareCallCount = 0; + + var pipeline = CreateTrackingPipeline(Prepare, _ => + { + executed = true; + return Task.CompletedTask; + }); + + // Start PrepareAsync but don't await completion + var prepareTask = pipeline.PrepareAsync(CancellationToken.None); + + await preparationStarted.Task; + + var runTask = pipeline.RunAsync(CancellationToken.None); + + continuePreparation.SetResult(true); + + await prepareTask; + await runTask; + + Assert.Equal(1, prepareCallCount); + Assert.True(executed); + return; + + async Task Prepare(CancellationToken token) + { + Interlocked.Increment(ref prepareCallCount); + preparationStarted.SetResult(true); + await continuePreparation.Task; + } + } + + [Fact] + public async Task RunAsync_ConcurrentCalls_OnlyFirstSucceeds() + { + var runCount = 0; + var barrier = new TaskCompletionSource(); + + var step = new TestStep(async _ => + { + Interlocked.Increment(ref runCount); + await barrier.Task; + }, ServiceProvider); + + var pipeline = CreatePipeline([step]); + + var firstTask = pipeline.RunAsync(CancellationToken.None); + + await Task.Delay(50, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + pipeline.RunAsync(CancellationToken.None)); + + barrier.SetResult(true); + await firstTask; + + Assert.Equal(1, runCount); + } + + [Fact] + public async Task RunAsync_AfterPrepare_ExecutesStep() + { + var executed = false; + var step = new TestStep(_ => + { + executed = true; + return Task.CompletedTask; + }, ServiceProvider); + var pipeline = CreatePipeline([step]); + + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.True(executed); + } + + [Fact] + public async Task RunAsync_Successful_PipelineFailedAndCancelledIsFalse() + { + var s = new TestStep(_ => Task.CompletedTask, ServiceProvider); + var pipeline = CreatePipeline([s]); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.False(pipeline.Failed); + Assert.False(pipeline.Cancelled); + } + + [Fact] + public async Task RunAsync_ThrowsNonCancellationException_SetsPipelineFailed() + { + var step = new TestStep(_ => throw new InvalidOperationException("Test error"), ServiceProvider); + var pipeline = CreatePipeline([step]); + + var record = await Record.ExceptionAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); + Assert.NotNull(record); + + + Assert.True(pipeline.Failed); + Assert.False(pipeline.Cancelled); + } + + [Fact] + public async Task RunAsync_ThrowsTaskCanceledException_SetsPipelineCancelled() + { + var step = new TestStep(_ => throw new TaskCanceledException(), ServiceProvider); + var pipeline = CreatePipeline([step]); + + await Assert.ThrowsAnyAsync(() => + pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.True(pipeline.Cancelled); + Assert.False(pipeline.Failed); + } + + [Fact] + public async Task RunAsync_ThrowsOperationCanceledException_SetsPipelineCancelled() + { + var step = new TestStep(_ => throw new OperationCanceledException(), ServiceProvider); + var pipeline = CreatePipeline([step]); + + await Assert.ThrowsAsync(() => + pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.True(pipeline.Cancelled); + Assert.False(pipeline.Failed); + } + + [Fact] + public async Task RunAsync_CancelledDuringPreparationViaExternalToken_SetsPipelineCancelled() + { + using var cts = new CancellationTokenSource(); + var waitToCancel = new TaskCompletionSource(); + var waitUntilCanceled = new ManualResetEvent(false); + + var pipeline = CreateTrackingPipeline(async ct => + { + await Task.Yield(); + waitToCancel.SetResult(true); + waitUntilCanceled.WaitOne(); + ct.ThrowIfCancellationRequested(); + }, _ => Task.CompletedTask); + + var runTask = pipeline.RunAsync(cts.Token); + await waitToCancel.Task; + + cts.Cancel(); + waitUntilCanceled.Set(); + + await Assert.ThrowsAnyAsync(() => runTask); + } + + [Fact] + public async Task RunAsync_ExternalTokenCancelledDuringExecution_SetsPipelineCancelled() + { + using var cts = new CancellationTokenSource(); + var waitToCancel = new TaskCompletionSource(); + var waitUntilCanceled = new ManualResetEvent(false); + + var step = new TestStep(async ct => + { + await Task.Yield(); + waitToCancel.SetResult(true); + waitUntilCanceled.WaitOne(); + ct.ThrowIfCancellationRequested(); + }, ServiceProvider); + + var pipeline = CreatePipeline([step]); + + var runTask = pipeline.RunAsync(cts.Token); + await waitToCancel.Task; + + cts.Cancel(); + waitUntilCanceled.Set(); + + await Assert.ThrowsAsync(() => runTask); + Assert.True(pipeline.Cancelled); + Assert.False(pipeline.Failed); + } + + #endregion + + #region Dispose Tests + + [Fact] + public async Task Dispose_DisposedPipeline_ThrowsOnPrepare() + { + var pipeline = CreatePipeline([]); + pipeline.Dispose(); + + Assert.True(pipeline.IsDisposed); + await Assert.ThrowsAsync(async () => await pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task Dispose_DisposedPipeline_ThrowsOnRun() + { + var pipeline = CreatePipeline([]); + pipeline.Dispose(); + + Assert.True(pipeline.IsDisposed); + await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task Dispose_CalledMultipleTimes_NoEffect() + { + var counter = 0; + var s = new TestStep(_ => + { + counter++; + return Task.CompletedTask; + }, ServiceProvider); + var pipeline = CreatePipeline([s]); + + pipeline.Dispose(); + pipeline.Dispose(); + pipeline.Dispose(); + + await Assert.ThrowsAsync(async () => await pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + Assert.Equal(0, counter); + Assert.False(pipeline.Failed); + } + + [Fact] + public async Task Dispose_AfterPrepare_ThrowsOnRun() + { + var executed = false; + var step = new TestStep(_ => + { + executed = true; + return Task.CompletedTask; + }, ServiceProvider); + var pipeline = CreatePipeline([step]); + + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + pipeline.Dispose(); + + await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.False(executed); + Assert.False(pipeline.Failed); + } + + [Fact] + public async Task Dispose_AfterSuccessfulRun_NoException() + { + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + var pipeline = CreatePipeline([step]); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + pipeline.Dispose(); + + Assert.True(pipeline.IsDisposed); + } + + [Fact] + public async Task Dispose_AfterFailedRun_NoException() + { + var step = new TestStep(_ => throw new InvalidOperationException(), ServiceProvider); + var pipeline = CreatePipeline([step]); + + var record = await Record.ExceptionAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + Assert.NotNull(record); + + pipeline.Dispose(); + + Assert.True(pipeline.IsDisposed); + } + + [Fact] + public async Task Dispose_DuringPreparation_ThrowsInvalidOperationException() + { + var preparationStarted = new TaskCompletionSource(); + var preparationDelay = new TaskCompletionSource(); + + var pipeline = CreateTrackingPipeline(async _ => + { + preparationStarted.SetResult(true); + await preparationDelay.Task; + }, _ => Task.CompletedTask); + + pipeline.PrepareAsync(TestContext.Current.CancellationToken).Forget(); + + await preparationStarted.Task; + + Assert.Throws(() => pipeline.Dispose()); + // The rest is undefined behavior... + } + + [Fact] + public void Dispose_DuringExecution_ThrowsInvalidOperationException() + { + var disposed = new TaskCompletionSource(); + var step = new TestStep(async _ => + { + await disposed.Task; + }, ServiceProvider); + var pipeline = CreatePipeline([step]); + + pipeline.RunAsync(TestContext.Current.CancellationToken).Forget(); + + Assert.Throws(() => pipeline.Dispose()); + // The rest is undefined behavior... + } + + #endregion + + #region Cancel Tests + + [Fact] + public async Task Cancel_DuringPreparation_HasNoEffect() + { + var waitToCancel = new TaskCompletionSource(); + var waitUntilCanceled = new ManualResetEvent(false); + + var pipeline = CreateTrackingPipeline(async ct => + { + await Task.Yield(); + waitToCancel.SetResult(true); + waitUntilCanceled.WaitOne(); + ct.ThrowIfCancellationRequested(); + }, _ => Task.CompletedTask); + + var prepareTask = pipeline.PrepareAsync(CancellationToken.None); + await waitToCancel.Task; + + pipeline.Cancel(); + waitUntilCanceled.Set(); + + var e = await Record.ExceptionAsync(() => prepareTask); + Assert.Null(e); + } + + [Fact] + public async Task Cancel_DuringWaitForPreparation_CancelsPipeline() + { + var waitToCancel = new TaskCompletionSource(); + var waitUntilCanceled = new ManualResetEvent(false); + + var pipeline = CreateTrackingPipeline(async ct => + { + await Task.Yield(); + waitToCancel.SetResult(true); + waitUntilCanceled.WaitOne(); + ct.ThrowIfCancellationRequested(); + }, _ => Task.CompletedTask); + + var runTask = pipeline.RunAsync(CancellationToken.None); + await waitToCancel.Task; + + pipeline.Cancel(); + waitUntilCanceled.Set(); + + await Assert.ThrowsAnyAsync(() => runTask); + } + + [Fact] + public async Task Cancel_DuringExecution_CancelsAndSetsPipelineFailed() + { + var waitToCancel = new TaskCompletionSource(); + var waitUntilCanceled = new ManualResetEvent(false); + var capturedToken = CancellationToken.None; + + var step = new TestStep(async ct => + { + await Task.Yield(); + capturedToken = ct; + waitToCancel.SetResult(true); + waitUntilCanceled.WaitOne(); + }, ServiceProvider); + + var pipeline = CreatePipeline([step]); + + await pipeline.PrepareAsync(CancellationToken.None); + + var pipelineTask = pipeline.RunAsync(CancellationToken.None); + await waitToCancel.Task; + + pipeline.Cancel(); + waitUntilCanceled.Set(); + + await Assert.ThrowsAsync(() => pipelineTask); + + Assert.False(pipeline.Failed); + Assert.True(pipeline.Cancelled); + Assert.True(capturedToken.IsCancellationRequested); + } + + [Fact] + public async Task Cancel_BeforeRun_HasNoEffect() + { + var executed = false; + var step = new TestStep(_ => + { + executed = true; + return Task.CompletedTask; + }, ServiceProvider); + var pipeline = CreatePipeline([step]); + + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + pipeline.Cancel(); + + await pipeline.RunAsync(CancellationToken.None); + + Assert.False(pipeline.Failed); + Assert.False(pipeline.Cancelled); + Assert.True(executed); + } + + [Fact] + public async Task Cancel_BeforePrepare_NoException() + { + var pipeline = CreatePipeline([]); + + pipeline.Cancel(); + + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.False(pipeline.Failed); + Assert.False(pipeline.Cancelled); + } + + [Fact] + public async Task Cancel_AfterRun_NoEffect() + { + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + var pipeline = CreatePipeline([step]); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + pipeline.Cancel(); // Should not throw + + Assert.False(pipeline.Failed); + Assert.False(pipeline.Cancelled); + } + + [Fact] + public async Task Cancel_CalledMultipleTimes_NoException() + { + var waitToCancel = new TaskCompletionSource(); + var waitUntilCanceled = new ManualResetEvent(false); + + var step = new TestStep(_ => + { + waitToCancel.SetResult(true); + waitUntilCanceled.WaitOne(); + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = CreatePipeline([step]); + + await pipeline.PrepareAsync(CancellationToken.None); + + var pipelineTask = pipeline.RunAsync(CancellationToken.None); + await waitToCancel.Task; + + pipeline.Cancel(); + pipeline.Cancel(); + pipeline.Cancel(); + + waitUntilCanceled.Set(); + + await Assert.ThrowsAsync(() => pipelineTask); + } + + + [Fact] + public void Cancel_OnDisposedPipeline_NoException() + { + var pipeline = CreatePipeline([]); + pipeline.Dispose(); + + pipeline.Cancel(); + + Assert.True(pipeline.IsDisposed); + } + + #endregion + + #region ToString Tests + + [Fact] + public void ToString_ReturnsTypeName() + { + var pipeline = CreatePipeline([]); + + var result = pipeline.ToString(); + + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + #endregion +} + +public interface ITrackingPipeline : IPipeline; \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs new file mode 100644 index 0000000..261b9a4 --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs @@ -0,0 +1,688 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using Xunit; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; + +public class ProducerConsumerPipelineTest : StepRunnerPipelineBaseTestBase +{ + protected override StepRunnerPipelineBase CreateStepRunnerPipelineBase(IList steps, bool failFast, RunnerBehavior runnerBehavior) + { + return CreateConsumerPipeline(steps.ToAsyncEnumerable(), failFast, runnerBehavior); + } + + protected override ITrackingPipeline CreateTrackingPipeline(Func prepare, Func run) + { + IEnumerable steps = [new TestStep(run, ServiceProvider)]; + return new TestProducerConsumerPipeline(ServiceProvider, steps.ToAsyncEnumerable(), prepare, GetWorkerCount(GetRandomRunBehavior()), false); + } + + private ProducerConsumerPipeline CreateConsumerPipeline(IAsyncEnumerable steps, bool failFast, RunnerBehavior runnerBehavior) + { + return new TestProducerConsumerPipeline(ServiceProvider, steps, null, GetWorkerCount(runnerBehavior), failFast); + } + + private ProducerConsumerPipeline CreateConsumerPipeline(IAsyncEnumerable steps) + { + return CreateConsumerPipeline(steps, Random.Bool(), GetRandomRunBehavior()); + } + + #region Constructor Tests + + [Fact] + public void Ctor_NullServiceProvider_Throws() + { + Assert.Throws(() => new TestProducerConsumerPipeline( + null!, AsyncEnumerable.Empty(), null, 4, true)); + } + + [Fact] + public void Ctor_InvalidWorkerCount_Throws() + { + Assert.Throws(() => new TestProducerConsumerPipeline( + ServiceProvider, AsyncEnumerable.Empty(), null, 0, true)); + Assert.Throws(() => new TestProducerConsumerPipeline( + ServiceProvider, AsyncEnumerable.Empty(), null, new Random().Next(int.MinValue, 0), true)); + Assert.Throws(() => new TestProducerConsumerPipeline( + ServiceProvider, AsyncEnumerable.Empty(), null, new Random().Next(65, int.MaxValue), true)); + } + + #endregion + + #region RunAsync + + [Fact] + public async Task RunAsync_ConcurrentCallsDuringBackgroundPreparation_OnlyFirstSucceeds() + { + var productionStarted = new TaskCompletionSource(); + var continueProduction = new TaskCompletionSource(); + var stepRunCount = 0; + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + // Start first RunAsync + var firstTask = pipeline.RunAsync(CancellationToken.None); + + // Wait for production/preparation to start + await productionStarted.Task; + + await Assert.ThrowsAsync(() => pipeline.RunAsync(CancellationToken.None)); + + continueProduction.SetResult(true); + await firstTask; + + Assert.Equal(1, stepRunCount); + Assert.False(pipeline.Failed); + + async IAsyncEnumerable ProduceStepsAsync() + { + productionStarted.SetResult(true); + await continueProduction.Task; + yield return new TestStep(_ => + { + Interlocked.Increment(ref stepRunCount); + return Task.CompletedTask; + }, ServiceProvider); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RunAsync_PreparationFails_ThrowsPreparationException(bool failFast) + { + var barrier = new ManualResetEventSlim(false); + var canThrow = new TaskCompletionSource(false); + + var ran = false; + var s1 = new TestStep(ct => + { + canThrow.SetResult(1); + barrier.Wait(ct); + ct.ThrowIfCancellationRequested(); + ran = true; + return Task.CompletedTask; + }, ServiceProvider); + var s2 = new TestStep(null, ServiceProvider); + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync(), failFast, GetRandomRunBehavior()); + + var task = Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + if (failFast) + await task; + + barrier.Set(); + await task; + + if (failFast) + Assert.False(ran); + else + Assert.True(ran); + + async IAsyncEnumerable ProduceStepsAsync() + { + yield return s1; + yield return s2; + await canThrow.Task; + throw new ApplicationException("test"); + } + } + + [Fact] + public async Task RunAsync_DelayedStepProduction_ExecutesAllSteps() + { + var tcs = new TaskCompletionSource(); + + var s1 = new TestStep(async _ => + { + await Task.Delay(100, TestContext.Current.CancellationToken); + tcs.SetResult(true); + }, ServiceProvider); + + var s2Run = false; + var s2 = new TestStep(_ => + { + s2Run = true; + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.True(s2Run); + + async IAsyncEnumerable ProduceStepsAsync() + { + yield return s1; + await tcs.Task; + yield return s2; + } + } + + [Theory] + [InlineData(RunnerBehavior.Concurrent)] + [InlineData(RunnerBehavior.Sequential)] + public async Task RunAsync_StepsProducedWhileRunning_AllExecuted(RunnerBehavior runnerBehavior) + { + var executedSteps = new ConcurrentQueue(); + const int stepCount = 10; + const int delay = 50; + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync(), Random.Bool(), runnerBehavior); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.Equal(stepCount, executedSteps.Count); + if (runnerBehavior is RunnerBehavior.Sequential) + Assert.Equal(executedSteps.ToList().OrderBy(x => x), executedSteps); + return; + + + async IAsyncEnumerable ProduceStepsAsync() + { + for (var i = 0; i < stepCount; i++) + { + var stepIndex = i; + yield return new TestStep(_ => + { + executedSteps.Enqueue(stepIndex); + return Task.CompletedTask; + }, ServiceProvider); + await Task.Delay(delay, TestContext.Current.CancellationToken); + } + } + } + + [Theory] + [InlineData(RunnerBehavior.Concurrent)] + [InlineData(RunnerBehavior.Sequential)] // Checks, production is started on ThreadPool + public async Task RunAsync_ConsumesStepsWhileProducing(RunnerBehavior runnerBehavior) + { + var executionStartedDuringProduction = false; + var firstStepExecutionStarted = new TaskCompletionSource(); + var canContinueProduction = new TaskCompletionSource(); + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync(), Random.Bool(), runnerBehavior); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.True(executionStartedDuringProduction, "Step execution should start before production completes"); + return; + + async IAsyncEnumerable ProduceStepsAsync() + { + // Produce first step + yield return new TestStep(async _ => + { + firstStepExecutionStarted.SetResult(true); + await canContinueProduction.Task; + }, ServiceProvider); + + // Wait for first step to start executing + await firstStepExecutionStarted.Task; + + // At this point, execution has started while we're still producing + executionStartedDuringProduction = true; + + // Produce more steps + yield return new TestStep(_ => Task.CompletedTask, ServiceProvider); + yield return new TestStep(_ => Task.CompletedTask, ServiceProvider); + + // Allow first step to complete + canContinueProduction.SetResult(true); + + } + } + + [Fact] + public async Task RunAsync_CalledDuringStepProduction_WaitsForSamePreparation() + { + var preparationStarted = new TaskCompletionSource(); + var continuePreparation = new TaskCompletionSource(); + var stepExecuted = false; + var prepareCallCount = 0; + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + var prepareTask = pipeline.PrepareAsync(CancellationToken.None); + + await preparationStarted.Task; + + var runTask = pipeline.RunAsync(CancellationToken.None); + + continuePreparation.SetResult(true); + + await prepareTask; + await runTask; + + Assert.Equal(1, prepareCallCount); + Assert.True(stepExecuted); + Assert.False(pipeline.Failed); + + async IAsyncEnumerable ProduceStepsAsync() + { + Interlocked.Increment(ref prepareCallCount); + preparationStarted.SetResult(true); + await continuePreparation.Task; + yield return new TestStep(_ => + { + stepExecuted = true; + return Task.CompletedTask; + }, ServiceProvider); + } + } + + [Fact] + public async Task RunAsync_PreparationCancelled_ThrowsOperationCanceledException() + { + var cts = new CancellationTokenSource(); + var tcs = new TaskCompletionSource(); + + var s1 = new TestStep(async _ => + { + await Task.Delay(100, TestContext.Current.CancellationToken); + tcs.SetResult(true); + }, ServiceProvider); + + var s2Run = false; + var s2 = new TestStep(_ => + { + s2Run = true; + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + await Assert.ThrowsAnyAsync(() => pipeline.RunAsync(cts.Token)); + + Assert.False(s2Run); + return; + + async IAsyncEnumerable ProduceStepsAsync() + { + yield return s1; + await tcs.Task; + cts.Cancel(); + await Task.Delay(100, TestContext.Current.CancellationToken); + yield return s2; + } + } + + [Fact] + public async Task RunAsync_CancelledDuringProduction_StopsProducing() + { + var cts = new CancellationTokenSource(); + var productionGate = new TaskCompletionSource(); + var cancelGate = new TaskCompletionSource(); + var producedSteps = new List(); + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + var runTask = Assert.ThrowsAnyAsync(() => pipeline.RunAsync(cts.Token)); + + await productionGate.Task; + + cts.Cancel(); + cancelGate.SetResult(true); + + await runTask; + + Assert.Equal(2, producedSteps.Count); + + async IAsyncEnumerable ProduceStepsAsync() + { + for (var i = 0; i < 10; i++) + { + if (i == 2) + { + productionGate.SetResult(true); + await cancelGate.Task; + } + + cts.Token.ThrowIfCancellationRequested(); + + producedSteps.Add(i); + yield return new TestStep(_ => Task.CompletedTask, ServiceProvider); + } + } + } + + [Fact] + public async Task RunAsync_PreparationFailsImmediately_ThrowsException() + { + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + async IAsyncEnumerable ProduceStepsAsync() + { + await Task.Yield(); + throw new ApplicationException("Immediate failure"); +#pragma warning disable CS0162 // Unreachable code detected + yield break; +#pragma warning restore CS0162 + } + } + + [Theory] + [InlineData(RunnerBehavior.Concurrent)] + [InlineData(RunnerBehavior.Sequential)] + public async Task RunAsync_PreparationAndExecutionConcurrent_NoDeadlock(RunnerBehavior runnerBehavior) + { + var firstStepStarted = new ManualResetEventSlim(false); + var allowPreparationToContinue = new ManualResetEventSlim(false); + var executedSteps = new ConcurrentBag(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, TestContext.Current.CancellationToken); + + + var pipeline = new NonAwaitingTestProducerConsumerPipeline(ServiceProvider, + GetWorkerCount(runnerBehavior), buildSteps: BuildSteps); + + var runTask = pipeline.RunAsync(linkedCts.Token); + + // Wait for execution to start with timeout + Assert.True(firstStepStarted.Wait(TimeSpan.FromSeconds(5), linkedCts.Token), + "First step did not start; pipeline may be deadlocked."); + + // Allow preparation to continue producing steps + allowPreparationToContinue.Set(); + + // Should complete without deadlock + await runTask; + + Assert.False(cts.IsCancellationRequested, "Pipeline hit timeout; possible deadlock."); + Assert.Equal(11, executedSteps.Count); + Assert.Contains(0, executedSteps); + return; + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + async IAsyncEnumerable BuildSteps([EnumeratorCancellation] CancellationToken token) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + // First step: starts execution, signals, then blocks + yield return new TestStep(async _ => + { + await Task.Yield(); + executedSteps.Add(0); + firstStepStarted.Set(); + + // Wait with timeout to avoid hanging + if (!allowPreparationToContinue.Wait(TimeSpan.FromSeconds(8), linkedCts.Token)) + throw new TimeoutException("First step timed out waiting for continuation signal"); + }, ServiceProvider); + + // Wait for first step to actually start executing with timeout + if (!firstStepStarted.Wait(TimeSpan.FromSeconds(5), linkedCts.Token)) + throw new TimeoutException("BuildSteps timed out waiting for first step to start"); + + // Produce more steps while first step is still running + for (var i = 1; i <= 10; i++) + { + var index = i; + yield return new TestStep(_ => + { + executedSteps.Add(index); + return Task.CompletedTask; + }, ServiceProvider); + } + } + } + + [Fact] + public async Task RunAsync_CancellationDoesNotCauseStepAddedProductionThrowsInvalidOperationException() + { + var productionStarted = new TaskCompletionSource(); + var enumerationCompleted = new TaskCompletionSource(); + + var pipeline = CreateConsumerPipeline( + CreateSteps(CancellationToken.None), + Random.Bool(), + RunnerBehavior.Sequential); + + var pipelineTask = pipeline.RunAsync(new CancellationToken(true)); + + await productionStarted.Task; + + var productionCompletedAndHandledTask = Task.Run(async () => + { + await enumerationCompleted.Task; + // Required, to ensure the ExceptionHandler of RunPreparationAsync had time to complete. + await Task.Delay(200, CancellationToken.None); + }, CancellationToken.None); + + var completed = await Task.WhenAny(productionCompletedAndHandledTask, Task.Delay(5000, CancellationToken.None)); + Assert.Equal(productionCompletedAndHandledTask, completed); + + await Assert.ThrowsAsync(async () => await pipelineTask); + + Assert.False(pipeline.Failed); + return; + + async IAsyncEnumerable CreateSteps([EnumeratorCancellation] CancellationToken _) + { + productionStarted.SetResult(true); + try + { + yield return new TestStep(_ => Task.CompletedTask, ServiceProvider); + } + finally + { + enumerationCompleted.TrySetResult(true); + } + } + } + + [Fact] + public async Task RunAsync_StepAddedAfterRunnerFinishedUnexpectedly_RethrowsInvalidOperationException() + { + var productionStarted = new TaskCompletionSource(); + var proceedWithProduction = new TaskCompletionSource(); + var enumerationCompleted = new TaskCompletionSource(); + + var pipeline = new TestProducerConsumerPipelineExposed( + ServiceProvider, + CreateSteps(CancellationToken.None), + prepareAction: null, + workerCount: 1, + failFast: false); + + var pipelineTask = pipeline.RunAsync(CancellationToken.None); + + await productionStarted.Task; + + // Finish the runner without cancellation request, + // which means that something really unexpected was going on + pipeline.ExposedStepRunner.Finish(); + + proceedWithProduction.SetResult(true); + + var productionCompletedAndHandledTask = Task.Run(async () => + { + await enumerationCompleted.Task; + // Required, to ensure the ExceptionHandler of RunPreparationAsync had time to complete. + await Task.Delay(200, CancellationToken.None); + }, CancellationToken.None); + + var completed = await Task.WhenAny(productionCompletedAndHandledTask, Task.Delay(5000, CancellationToken.None)); + Assert.Equal(productionCompletedAndHandledTask, completed); + + await Assert.ThrowsAsync(async () => await pipelineTask); + + Assert.True(pipeline.Failed); + return; + + async IAsyncEnumerable CreateSteps([EnumeratorCancellation] CancellationToken _) + { + productionStarted.SetResult(true); + await proceedWithProduction.Task; + + try + { + yield return new TestStep(_ => Task.CompletedTask, ServiceProvider); + } + finally + { + enumerationCompleted.TrySetResult(true); + } + } + } + + #endregion + + #region PrepareAsync + + [Fact] + public async Task PrepareAsync_ConcurrentCallsDuringStepProduction_OnlyFirstSucceeds() + { + var productionStarted = new TaskCompletionSource(); + var continueProduction = new TaskCompletionSource(); + var prepareCount = 0; + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + var firstTask = pipeline.PrepareAsync(CancellationToken.None); + + await productionStarted.Task; + + await Assert.ThrowsAsync(() => pipeline.PrepareAsync(CancellationToken.None)); + + continueProduction.SetResult(true); + await firstTask; + + Assert.Equal(1, prepareCount); + + async IAsyncEnumerable ProduceStepsAsync() + { + Interlocked.Increment(ref prepareCount); + productionStarted.SetResult(true); + await continueProduction.Task; + yield return new TestStep(_ => Task.CompletedTask, ServiceProvider); + } + } + + [Fact] + public async Task PrepareAsync_CalledAfterRunAsyncStartedPreparation_ThrowsInvalidOperationException() + { + var preparationStarted = new TaskCompletionSource(); + var continuePreparation = new TaskCompletionSource(); + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + var runTask = pipeline.RunAsync(CancellationToken.None); + + await preparationStarted.Task; + + await Assert.ThrowsAsync(() => pipeline.PrepareAsync(CancellationToken.None)); + + continuePreparation.SetResult(true); + await runTask; + + Assert.False(pipeline.Failed); + + async IAsyncEnumerable ProduceStepsAsync() + { + preparationStarted.SetResult(true); + await continuePreparation.Task; + yield return new TestStep(_ => Task.CompletedTask, ServiceProvider); + } + } + + [Fact] + public async Task PrepareAsync_PreparationFails_ThrowsException() + { + var s1 = new TestStep(null, ServiceProvider); + var s2 = new TestStep(null, ServiceProvider); + + var pipeline = CreateConsumerPipeline(ProduceStepsAsync()); + + await Assert.ThrowsAsync(async () => await pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + + async IAsyncEnumerable ProduceStepsAsync() + { + yield return s1; + yield return s2; + await Task.Yield(); + throw new ApplicationException("test"); + } + } + + [Fact] + public async Task PrepareAsync_RunnerInitializedWithCtorWorkerCount() + { + var workerCount = new Random().Next(1, 65); + var pipeline = new TestProducerConsumerPipeline(ServiceProvider, Array.Empty().ToAsyncEnumerable(), + null, workerCount, false); + + await pipeline.PrepareAsync(CancellationToken.None); + + Assert.Equal(workerCount, pipeline.StepRunner.WorkerCount); + } + + #endregion + + private class TestProducerConsumerPipeline : ProducerConsumerPipeline, ITrackingPipeline + { + private readonly IAsyncEnumerable _steps; + private readonly Func? _prepareAction; + + public TestProducerConsumerPipeline( + IServiceProvider serviceProvider, + IAsyncEnumerable steps, + Func? onPrepare, + int workerCount, + bool failFast) : base(workerCount, serviceProvider) + { + _steps = steps; + FailFast = failFast; + _prepareAction = onPrepare; + } + + protected override async IAsyncEnumerable BuildStepsAsync([EnumeratorCancellation] CancellationToken token) + { + if (_prepareAction is not null) + await _prepareAction(token); + await foreach (var step in _steps.WithCancellation(token)) + { + //token.ThrowIfCancellationRequested(); + yield return step; + } + } + } + + private class NonAwaitingTestProducerConsumerPipeline( + IServiceProvider serviceProvider, + int workerCount, + Func> buildSteps) + : ProducerConsumerPipeline(workerCount, serviceProvider) + { + private readonly Func> _buildSteps = buildSteps ?? throw new ArgumentNullException(nameof(buildSteps)); + + protected override IAsyncEnumerable BuildStepsAsync(CancellationToken token) + { + return _buildSteps(token); + } + } + + private class TestProducerConsumerPipelineExposed( + IServiceProvider serviceProvider, + IAsyncEnumerable steps, + Func? prepareAction, + int workerCount, + bool failFast) + : TestProducerConsumerPipeline(serviceProvider, steps, prepareAction, workerCount, failFast) + { + public ProducerConsumerStepRunner ExposedStepRunner => StepRunner; + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/RunnerBehavior.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/RunnerBehavior.cs new file mode 100644 index 0000000..805702b --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/RunnerBehavior.cs @@ -0,0 +1,7 @@ +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; + +public enum RunnerBehavior +{ + Sequential = 0, + Concurrent = 1 +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs index 67f3169..a0fdfe0 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs @@ -1,71 +1,67 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Threading; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; +using AnakinRaW.CommonUtilities.Testing.Extensions; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -public class SequentialPipelineTests : StepRunnerPipelineTest +public class SequentialPipelineTests : StepRunnerPipelineTestBase { - protected override StepRunnerPipeline CreatePipeline(IList steps, bool failFast) + protected override bool RunnerSupportsConcurrentRuns => false; + + protected override StepRunnerPipeline CreateStepRunnerPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior) { - return new TestSequentialPipeline(steps, ServiceProvider, failFast); + if (runnerBehavior is RunnerBehavior.Concurrent) + throw new NotSupportedException("Concurrent runs are not supported"); + return CreateSequentialPipeline(steps, failFast); } - protected override Pipeline CreatePipeline(IList steps) + protected override ITrackingPipeline CreateTrackingPipeline(Func prepare, Func run) { - return CreatePipeline(steps, true); + var testStep = new TestStep(run, ServiceProvider); + return new TestSequentialPipeline(ServiceProvider, [testStep], prepare, failFast: false); } - [Fact] - public void Ctor_NullArgs_Throws() + private SequentialPipeline CreateSequentialPipeline(IList steps, bool failFast) { - Assert.Throws(() => new TestSequentialPipeline([], null!)); + return new TestSequentialPipeline(ServiceProvider, steps, null, failFast); } - [Fact] - public async Task RunAsync_RunsInSequence() - { - var sb = new StringBuilder(); - - var s1 = new TestStep(_ => sb.Append('a'), ServiceProvider); - var s2 = new TestStep(_ => sb.Append('b'), ServiceProvider); + #region Constructor Tests - var pipeline = CreatePipeline([s1, s2], true); - - await pipeline.RunAsync(TestContext.Current.CancellationToken); - Assert.Equal("ab", sb.ToString()); - - Assert.False(pipeline.PipelineFailed); + [Fact] + public void Ctor_NullServiceProvider_Throws() + { + Assert.Throws(() => new TestSequentialPipeline(null!, [], null, Random.Bool())); } - [Theory] - [InlineData(true, "")] - //[InlineData(false, "b")] - public async Task RunAsync_WithError_FailFastBehavior_Throws(bool failFast, string result) + #endregion + + private class TestSequentialPipeline : SequentialPipeline, ITrackingPipeline { - var sb = new StringBuilder(); + private readonly IList _steps; + private readonly Func? _prepareAction; - var s1 = new TestStep(_ => throw new Exception("Test"), ServiceProvider); - var s2 = new TestStep(_ => sb.Append('b'), ServiceProvider); - - var pipeline = CreatePipeline([s1, s2], failFast); - - var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); - Assert.Equal("Step 'TestStep' failed with error: Test", e.Message); - Assert.Equal(result, sb.ToString()); - Assert.True(pipeline.PipelineFailed); - } + public TestSequentialPipeline( + IServiceProvider serviceProvider, + IList steps, + Func? onPrepare, + bool failFast = false) + : base(serviceProvider) + { + _steps = steps; + FailFast = failFast; + _prepareAction = onPrepare; + } - private class TestSequentialPipeline(IEnumerable steps, IServiceProvider serviceProvider, bool failFast = true) - : SequentialPipeline(serviceProvider, failFast) - { - protected override Task> BuildSteps() + protected override async Task> CreateRunnerSteps(CancellationToken token) { - return Task.FromResult>(steps.ToList()); + if (_prepareAction is not null) + await _prepareAction(token); + return _steps; } } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs new file mode 100644 index 0000000..2aae8fa --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs @@ -0,0 +1,553 @@ +using AnakinRaW.CommonUtilities.Testing.Extensions; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; +using Xunit; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; + +public abstract class StepRunnerPipelineBaseTestBase : PipelineTestBase where TRunner : class, IStepRunner +{ + protected virtual bool RunnerSupportsSequentialRuns => true; + + protected virtual bool RunnerSupportsConcurrentRuns => true; + + protected abstract StepRunnerPipelineBase CreateStepRunnerPipelineBase( + IList steps, + bool failFast, + RunnerBehavior runnerBehavior); + + protected StepRunnerPipelineBase CreateStepRunnerPipelineBase(IList steps) + { + var sequential = GetRandomRunBehavior(); + return CreateStepRunnerPipelineBase(steps, Random.Bool(), sequential); + } + + protected StepRunnerPipelineBase CreateStepRunnerPipelineBase(IList steps, bool failFast) + { + var sequential = GetRandomRunBehavior(); + return CreateStepRunnerPipelineBase(steps, failFast, sequential); + } + + protected override Pipeline CreatePipeline(IList steps) + { + return CreateStepRunnerPipelineBase(steps, false); + } + + protected RunnerBehavior GetRandomRunBehavior() + { + if (RunnerSupportsConcurrentRuns && RunnerSupportsSequentialRuns) + return Random.Enum(); + if (RunnerSupportsConcurrentRuns) + return RunnerBehavior.Concurrent; + if (RunnerSupportsSequentialRuns) + return RunnerBehavior.Sequential; + throw new NotSupportedException(); + } + + protected virtual int GetWorkerCount(RunnerBehavior runnerBehavior) + { + return runnerBehavior is RunnerBehavior.Sequential ? 1 : 4; + } + + protected bool IsRunBehaviorSupported(RunnerBehavior runnerBehavior) + { + switch (runnerBehavior) + { + case RunnerBehavior.Sequential when !RunnerSupportsSequentialRuns: + case RunnerBehavior.Concurrent when !RunnerSupportsConcurrentRuns: + return false; + } + + return true; + } + + #region FailFast + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void FailFast_CtorSetsProperty(bool failFast) + { + var pipeline = CreateStepRunnerPipelineBase([], failFast); + Assert.Equal(failFast, pipeline.FailFast); + } + + #endregion + + #region StepRunner + + [Fact] + public void StepRunner_AccessAlwaysInitializes() + { + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + var pipeline = CreateStepRunnerPipelineBase([step], Random.Bool()); + Assert.NotNull(pipeline.StepRunner); + } + + [Fact] + public async Task StepRunner_IsNotNullAfterPreparation() + { + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + var pipeline = CreateStepRunnerPipelineBase([step], Random.Bool()); + + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + + Assert.NotNull(pipeline.StepRunner); + } + + #endregion + + #region IsStepRunnerInitialized + + [Fact] + public async Task IsStepRunnerInitialized_IsInitializedAfterPrepare() + { + var pipeline = CreateStepRunnerPipelineBase([], Random.Bool()); + Assert.False(pipeline.IsStepRunnerInitialized); + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + Assert.True(pipeline.IsStepRunnerInitialized); + } + + [Fact] + public async Task IsStepRunnerInitialized_IsInitializedAfterRun() + { + var pipeline = CreateStepRunnerPipelineBase([], Random.Bool()); + Assert.False(pipeline.IsStepRunnerInitialized); + await pipeline.RunAsync(TestContext.Current.CancellationToken); + Assert.True(pipeline.IsStepRunnerInitialized); + } + + #endregion + + #region RunAsync + + [Fact] + public async Task RunAsync_EmptyPipeline_Succeeds() + { + var pipeline = CreateStepRunnerPipelineBase([]); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.False(pipeline.Failed); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RunAsync_AllStepsExecuted(bool prepare) + { + var runCounter = 0; + + var s1 = new TestStep(_ => + { + Interlocked.Increment(ref runCounter); + return Task.CompletedTask; + }, ServiceProvider); + var s2 = new TestStep(_ => + { + Interlocked.Increment(ref runCounter); + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = CreateStepRunnerPipelineBase([s1, s2]); + + if (prepare) + await pipeline.PrepareAsync(TestContext.Current.CancellationToken); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.Equal(2, runCounter); + Assert.False(pipeline.Failed); + } + + [Fact] + public async Task RunAsync_StepReceivesValidCancellationToken() + { + var receivedToken = CancellationToken.None; + var tokenWasProvided = false; + + var step = new TestStep(ct => + { + receivedToken = ct; + tokenWasProvided = ct.CanBeCanceled; + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = CreateStepRunnerPipelineBase([step]); + + using var cts = new CancellationTokenSource(); + await pipeline.RunAsync(cts.Token); + + Assert.True(tokenWasProvided); + Assert.True(receivedToken.CanBeCanceled); + } + + [Theory] + [InlineData(RunnerBehavior.Sequential)] + [InlineData(RunnerBehavior.Concurrent)] + public async Task RunAsync_FailFastEnabled_StepThrows_StopsExecution(RunnerBehavior runnerBehavior) + { + if (!IsRunBehaviorSupported(runnerBehavior)) + return; + + var secondStepRan = false; + var s1 = new TestStep(_ => throw new Exception("Test"), ServiceProvider); + var s2 = new TestStep(_ => + { + secondStepRan = true; + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = CreateStepRunnerPipelineBase([s1, s2], true, runnerBehavior); + + await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.True(pipeline.Failed); + + if (runnerBehavior is RunnerBehavior.Sequential) + { + Assert.False(secondStepRan, "FailFast should prevent subsequent steps from running"); + Assert.True(pipeline.Cancelled); + } + } + + [Theory] + [InlineData(RunnerBehavior.Sequential)] + [InlineData(RunnerBehavior.Concurrent)] + public async Task RunAsync_FailFastDisabled_ContinuesAfterError(RunnerBehavior runnerBehavior) + { + if (!IsRunBehaviorSupported(runnerBehavior)) + return; + + var executed = new ConcurrentQueue(); + + var s1 = new TestStep(_ => { executed.Enqueue(1); return Task.CompletedTask; }, ServiceProvider); + var s2 = new TestStep(_ => { executed.Enqueue(2); throw new InvalidOperationException(); }, ServiceProvider); + var s3 = new TestStep(_ => { executed.Enqueue(3); return Task.CompletedTask; }, ServiceProvider); + + var pipeline = CreateStepRunnerPipelineBase([s1, s2, s3], false, runnerBehavior); + + await Assert.ThrowsAsync( + () => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + if (runnerBehavior is RunnerBehavior.Sequential) + Assert.Equal([1, 2, 3], executed); + else + Assert.EqualUnordered([1, 2, 3], executed.ToList()); + } + + [Fact] + public async Task RunAsync_StepThrowsException_ThrowsStepFailureException() + { + var step = new TestStep(_ => throw new InvalidOperationException("Test error"), ServiceProvider); + var pipeline = CreatePipeline([step]); + + var e = await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.True(pipeline.Failed); + Assert.False(pipeline.Cancelled); + Assert.Contains("failed with error", e.Message); + Assert.Contains("Test error", e.Message); + } + + [Fact] + public async Task RunAsync_MultipleStepsThrow_AllErrorsCaptured() + { + var s1 = new TestStep(_ => throw new Exception("Error1"), ServiceProvider); + var s2 = new TestStep(_ => throw new Exception("Error2"), ServiceProvider); + + var pipeline = CreateStepRunnerPipelineBase([s1, s2], false); + + var e = await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.True(pipeline.Failed); + Assert.Contains("failed with error", e.Message); + Assert.Contains("Error1", e.Message); + Assert.Contains("Error2", e.Message); + } + + [Fact] + public async Task RunAsync_WithPreCancelledToken_ThrowsAndSetsPipelineCancelled() + { + var executed = false; + var step = new TestStep(_ => + { + executed = true; + return Task.CompletedTask; + }, ServiceProvider); + var pipeline = CreatePipeline([step]); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(() => pipeline.RunAsync(cts.Token)); + + Assert.False(executed); + Assert.True(pipeline.Cancelled); + Assert.False(pipeline.Failed); + } + + [Fact] + public async Task RunAsync_StepThrowsOperationCanceledException_TreatedAsCancellation() + { + var s1 = new TestStep(_ => throw new OperationCanceledException(), ServiceProvider); + + var pipeline = CreatePipeline([s1]); + + // OperationCanceledException from a step should propagate + await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.True(pipeline.Cancelled); + Assert.False(pipeline.Failed); + } + + [Fact] + public async Task RunAsync_StepThrowsAggregateExceptionWithCancellation_TreatedAsCancellation() + { + var step = new TestStep(_ => throw new AggregateException(new OperationCanceledException()), ServiceProvider); + + var pipeline = CreatePipeline([step]); + + await Assert.ThrowsAsync(() => + pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.False(pipeline.Failed); + Assert.True(pipeline.Cancelled); + } + + [Fact] + public async Task RunAsync_Concurrent_ExecutesStepsInParallel() + { + if (!RunnerSupportsConcurrentRuns) + return; + + var concurrentCount = 0; + var maxConcurrent = 0; + var lockObj = new object(); + + var steps = new List(); + for (var i = 0; i < GetWorkerCount(RunnerBehavior.Concurrent) * 10; i++) + { + steps.Add(new TestStep(async _ => + { + lock (lockObj) + { + concurrentCount++; + maxConcurrent = Math.Max(maxConcurrent, concurrentCount); + } + + await Task.Delay(100, TestContext.Current.CancellationToken); + + lock (lockObj) + { + concurrentCount--; + } + }, ServiceProvider)); + } + + var pipeline = CreateStepRunnerPipelineBase(steps, Random.Bool(), RunnerBehavior.Concurrent); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.True(maxConcurrent > 1, $"Expected parallel execution, but max concurrent was {maxConcurrent}"); + } + + [Fact] + public async Task RunAsync_Concurrent_RespectsWorkerCount() + { + if (!RunnerSupportsConcurrentRuns) + return; + + var executedSteps = new ConcurrentBag(); + var concurrentCount = 0; + var maxConcurrent = 0; + var lockObj = new object(); + + var steps = new List(); + + var stepCount = GetWorkerCount(RunnerBehavior.Concurrent) * 10; + for (var i = 1; i <= stepCount; i++) + { + var i1 = i; + steps.Add(new TestStep(async _ => + { + lock (lockObj) + { + concurrentCount++; + maxConcurrent = Math.Max(maxConcurrent, concurrentCount); + } + + await Task.Delay(new Random().Next(50, 300), TestContext.Current.CancellationToken); + executedSteps.Add(i1); + + lock (lockObj) + { + concurrentCount--; + } + }, ServiceProvider)); + } + + var pipeline = CreateStepRunnerPipelineBase(steps, Random.Bool(), RunnerBehavior.Concurrent); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.True(maxConcurrent <= pipeline.StepRunner.WorkerCount, + $"Expected max {pipeline.StepRunner.WorkerCount} concurrent, but was {maxConcurrent}"); + + Assert.Equal(stepCount, executedSteps.Count); + Assert.Equal(stepCount * (stepCount + 1) / 2, executedSteps.Sum(x => x)); + } + + [Fact] + public async Task RunAsync_Sequential_MultipleSteps() + { + if (!RunnerSupportsSequentialRuns) + return; + + var sb = new StringBuilder(); + + var s1 = new TestStep(_ => { sb.Append('a'); return Task.CompletedTask; }, ServiceProvider); + var s2 = new TestStep(async _ => + { + await Task.Delay(100, TestContext.Current.CancellationToken); + sb.Append('b'); + }, ServiceProvider); + var s3 = new TestStep(_ => { sb.Append('c'); return Task.CompletedTask; }, ServiceProvider); + + var pipeline = CreateStepRunnerPipelineBase([s1, s2, s3], Random.Bool(), RunnerBehavior.Sequential); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.Equal("abc", sb.ToString()); + Assert.False(pipeline.Failed); + } + + [Theory] + [InlineData(true, RunnerBehavior.Concurrent)] + [InlineData(true, RunnerBehavior.Sequential)] + [InlineData(false, RunnerBehavior.Concurrent)] + [InlineData(false, RunnerBehavior.Sequential)] + public async Task RunAsync_CancelledMidSequence_StopsOnFailFast(bool failFast, RunnerBehavior runnerBehavior) + { + if (!IsRunBehaviorSupported(runnerBehavior)) + return; + + using var cts = new CancellationTokenSource(); + var executedSteps = new ConcurrentQueue(); + + var s1 = new TestStep(_ => { executedSteps.Enqueue(1); return Task.CompletedTask; }, ServiceProvider); + var s2 = new TestStep(_ => + { + executedSteps.Enqueue(2); + cts.Cancel(); + return Task.CompletedTask; + }, ServiceProvider); + var s3 = new TestStep(_ => { executedSteps.Enqueue(3); return Task.CompletedTask; }, ServiceProvider); + + var pipeline = CreateStepRunnerPipelineBase([s1, s2, s3], failFast, runnerBehavior); + + await Assert.ThrowsAsync(() => pipeline.RunAsync(cts.Token)); + + if (runnerBehavior is RunnerBehavior.Sequential) + Assert.Equal([1, 2], executedSteps); + else + Assert.Contains(2, executedSteps); + } + + #endregion + + #region Common Usage Tests + + [Theory] + [InlineData(RunnerBehavior.Concurrent)] + [InlineData(RunnerBehavior.Sequential)] + public async Task UsageTest_StepsWaitingForEachOther(RunnerBehavior runnerBehavior) + { + if (!IsRunBehaviorSupported(runnerBehavior)) + return; + + var executedSteps = new ConcurrentQueue(); + + var s1 = new TestStep(async _ => + { + await Task.Delay(200, TestContext.Current.CancellationToken); + executedSteps.Enqueue(1); + }, ServiceProvider); + var s2 = new TestStep(async _ => + { + await s1; + executedSteps.Enqueue(2); + }, ServiceProvider); + var s3 = new TestStep(_ => + { + executedSteps.Enqueue(3); + return Task.CompletedTask; + }, ServiceProvider); + + + var pipeline = CreateStepRunnerPipelineBase([s1, s2, s3], Random.Bool(), runnerBehavior); + + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + if (runnerBehavior == RunnerBehavior.Sequential) + Assert.Equal([1,2,3], executedSteps); + else + { + var list = executedSteps.ToList(); + Assert.True(list.IndexOf(1) < list.IndexOf(2), "1 should appear before 2"); + } + } + + [Theory] + [InlineData(RunnerBehavior.Concurrent)] + [InlineData(RunnerBehavior.Sequential)] + public async Task UsageTest_StepsWaitingForEachOther_WrongInsertionOrderHangsOnSequential_WorksForConcurrentRun(RunnerBehavior runnerBehavior) + { + if (!IsRunBehaviorSupported(runnerBehavior)) + return; + + var executedSteps = new ConcurrentQueue(); + + var s1 = new TestStep(_ => + { + executedSteps.Enqueue(1); + return Task.CompletedTask; + }, ServiceProvider); + var s2 = new TestStep(async _ => + { + await s1; + executedSteps.Enqueue(2); + }, ServiceProvider); + var s3 = new TestStep(_ => + { + executedSteps.Enqueue(3); + return Task.CompletedTask; + }, ServiceProvider); + + // Insertion order for Sequential runs is broken and will cause starvation + var pipeline = CreateStepRunnerPipelineBase([s2, s1, s3], Random.Bool(), runnerBehavior); + + if (runnerBehavior == RunnerBehavior.Sequential) + { + var runTask = pipeline.RunAsync(TestContext.Current.CancellationToken); + var finished = await Task.WhenAny( + runTask, + Task.Delay(5000, TestContext.Current.CancellationToken)); + Assert.NotEqual(runTask, finished); + } + else + { + await pipeline.RunAsync(TestContext.Current.CancellationToken); + var list = executedSteps.ToList(); + Assert.True(list.IndexOf(1) < list.IndexOf(2), "1 should appear before 2"); + } + } + + #endregion +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTest.cs deleted file mode 100644 index d0ca0b3..0000000 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTest.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; - -public abstract class StepRunnerPipelineTest : PipelineTest where T : IStepRunner -{ - protected abstract StepRunnerPipeline CreatePipeline(IList steps, bool failFast); - - [Fact] - public async Task RunAsync_EmptyPipeline() - { - var pipeline = CreatePipeline([], true); - - await pipeline.RunAsync(TestContext.Current.CancellationToken); - Assert.False(pipeline.PipelineFailed); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task RunAsync_AllStepsExecuted(bool prepare) - { - var runCounter = 0; - - var s1 = new TestStep(_ => Interlocked.Increment(ref runCounter), ServiceProvider); - var s2 = new TestStep(_ => Interlocked.Increment(ref runCounter), ServiceProvider); - - var pipeline = CreatePipeline([s1, s2], true); - - if (prepare) - { - await pipeline.PrepareAsync(); - await pipeline.PrepareAsync(); // Double prepare should have no effect - } - - await pipeline.RunAsync(TestContext.Current.CancellationToken); - Assert.Equal(2, runCounter); - Assert.False(pipeline.PipelineFailed); - } - - [Fact] - public async Task RunAsync_WithError_Throws() - { - var s1 = new TestStep(_ => throw new Exception("Test"), ServiceProvider); - - var pipeline = CreatePipeline([s1], true); - - var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); - Assert.Equal("Step 'TestStep' failed with error: Test", e.Message); - Assert.True(pipeline.PipelineFailed); - } - - [Fact] - public async Task RunAsync_WithError_FailSlow_Throws() - { - var ran = false; - var s1 = new TestStep(_ => throw new Exception("Test"), ServiceProvider); - var s2 = new TestStep(_ => ran = true, ServiceProvider); - - var pipeline = CreatePipeline([s1, s2], false); - - var e = await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); - Assert.Equal("Step 'TestStep' failed with error: Test", e.Message); - Assert.True(pipeline.PipelineFailed); - Assert.True(ran); - } - - [Fact] - public async Task PrepareAsync_ReturnsNull_Throws() - { - var pipeline = new NullRunnerPipeline(ServiceProvider); - - await Assert.ThrowsAsync(pipeline.PrepareAsync); - - // Should not throw, as preparation is only done once. - await pipeline.PrepareAsync(); - - Assert.False(pipeline.PipelineFailed); - } - - private class NullRunnerPipeline(IServiceProvider serviceProvider) : StepRunnerPipeline(serviceProvider) - { - protected override T CreateRunner() - { - return default!; - } - - protected override Task> BuildSteps() - { - return Task.FromResult>([]); - } - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs new file mode 100644 index 0000000..8bb72cd --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; + +public abstract class StepRunnerPipelineTestBase : StepRunnerPipelineBaseTestBase +{ + protected abstract StepRunnerPipeline CreateStepRunnerPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior); + + protected override StepRunnerPipelineBase CreateStepRunnerPipelineBase(IList steps, bool failFast, RunnerBehavior runnerBehavior) + { + return CreateStepRunnerPipeline(steps, failFast, runnerBehavior); + } + + #region CreateRunner + + [Fact] + public virtual async Task CreateRunner_ReturnsNull_DuringPrepare_ThrowsInvalidOperationException() + { + var pipeline = new NullRunnerPipeline(ServiceProvider); + + await Assert.ThrowsAsync(async ()=> await pipeline.PrepareAsync(TestContext.Current.CancellationToken)); + + Assert.False(pipeline.Failed); + } + + [Fact] + public virtual async Task CreateRunner_ReturnsNull_DuringRun_ThrowsInvalidOperationException() + { + var pipeline = new NullRunnerPipeline(ServiceProvider); + + await Assert.ThrowsAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.True(pipeline.Failed); + } + + #endregion + + private class NullRunnerPipeline(IServiceProvider serviceProvider) : StepRunnerPipeline(serviceProvider) + { + protected override IStepRunner CreateRunner() + { + return null!; + } + + protected override Task> CreateRunnerSteps(CancellationToken token) + { + return Task.FromResult>([]); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs b/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs index 74885e6..33f43e2 100644 --- a/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Progress/AggregatedProgressReporterTests.cs @@ -1,52 +1,118 @@ using System; using System.Collections.Generic; using AnakinRaW.CommonUtilities.SimplePipeline.Progress; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using AnakinRaW.CommonUtilities.Testing; using Xunit; // ReSharper disable InconsistentNaming +// ReSharper disable UnusedMember.Global namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Progress; -// ReSharper disable once UnusedMember.Global +#region Test Classes for AggregatedProgressReporter + public class AggregatedProgressReporterTest_Struct : AggregatedProgressReporterTestBase { protected override TestInfoStruct CreateCustomProgressInfo(TestProgressStep step, double progress) + => new() { Progress = progress }; + + protected override ITestableAggregatedReporter CreateReporter(IEnumerable> steps) + => new AggregateTestReporter(InternalReporter, steps); + + protected override ITestableAggregatedReporter CreateReporterWithComparer(IEnumerable> steps) + => new AggregateTestReporter(InternalReporter, steps, new TestStepEqualityComparer()); + + public override void Ctor_NullArgs_Throws() { - return new TestInfoStruct - { - Progress = progress, - }; + Assert.Throws(() => new AggregateTestReporter(null!, [])); + Assert.Throws(() => new AggregateTestReporter(InternalReporter, null!)); + Assert.Throws(() => new AggregateTestReporter(null!, [], EqualityComparer>.Default)); + Assert.Throws(() => new AggregateTestReporter(InternalReporter, null!, EqualityComparer>.Default)); + Assert.Throws(() => new AggregateTestReporter(InternalReporter, [], null!)); } } -// ReSharper disable once UnusedMember.Global -public class AggregatedProgressReporterTest_Class: AggregatedProgressReporterTestBase +public class AggregatedProgressReporterTest_Class : AggregatedProgressReporterTestBase { protected override TestInfoClass CreateCustomProgressInfo(TestProgressStep step, double progress) + => new() { Progress = progress }; + + protected override ITestableAggregatedReporter CreateReporter(IEnumerable> steps) + => new AggregateTestReporter(InternalReporter, steps); + + protected override ITestableAggregatedReporter CreateReporterWithComparer(IEnumerable> steps) + => new AggregateTestReporter(InternalReporter, steps, new TestStepEqualityComparer()); + + public override void Ctor_NullArgs_Throws() { - return new TestInfoClass - { - Progress = progress, - }; + Assert.Throws(() => new AggregateTestReporter(null!, [])); + Assert.Throws(() => new AggregateTestReporter(InternalReporter, null!)); + Assert.Throws(() => new AggregateTestReporter(null!, [], EqualityComparer>.Default)); + Assert.Throws(() => new AggregateTestReporter(InternalReporter, null!, EqualityComparer>.Default)); + Assert.Throws(() => new AggregateTestReporter(InternalReporter, [], null!)); } } -public abstract class AggregatedProgressReporterTestBase : TestBaseWithServiceProvider where T : ITestInfo, new() +#endregion + +#region Test Classes for AggregatedProgressReporter + +public class AggregatedProgressReporterSimpleTest_Struct : AggregatedProgressReporterTestBase { - private readonly TestProgressReporter _internalReporter = new(); + protected override TestInfoStruct CreateCustomProgressInfo(TestProgressStep step, double progress) + => new() { Progress = progress }; - protected abstract T CreateCustomProgressInfo(TestProgressStep step, double progress); + protected override ITestableAggregatedReporter CreateReporter(IEnumerable> steps) + => new AggregateTestReporterSimple(InternalReporter, steps); - [Fact] - public void Ctor_NullArgs_Throws() + protected override ITestableAggregatedReporter CreateReporterWithComparer(IEnumerable> steps) + => new AggregateTestReporterSimple(InternalReporter, steps, new TestProgressStepEqualityComparer()); + + public override void Ctor_NullArgs_Throws() { - Assert.Throws(() => new AggregateTestReporter(null!, [])); - Assert.Throws(() => new AggregateTestReporter(_internalReporter, null!)); + Assert.Throws(() => new AggregateTestReporterSimple(null!, [])); + Assert.Throws(() => new AggregateTestReporterSimple(InternalReporter, null!)); + Assert.Throws(() => new AggregateTestReporterSimple(null!, [], EqualityComparer>.Default)); + Assert.Throws(() => new AggregateTestReporterSimple(InternalReporter, null!, EqualityComparer>.Default)); + Assert.Throws(() => new AggregateTestReporterSimple(InternalReporter, [], null!)); + } +} - Assert.Throws(() => new AggregateTestReporter(null!, [], EqualityComparer.Default)); - Assert.Throws(() => new AggregateTestReporter(_internalReporter, null!, EqualityComparer.Default)); - Assert.Throws(() => new AggregateTestReporter(_internalReporter, [], null!)); +public class AggregatedProgressReporterSimpleTest_Class : AggregatedProgressReporterTestBase +{ + protected override TestInfoClass CreateCustomProgressInfo(TestProgressStep step, double progress) + => new() { Progress = progress }; + + protected override ITestableAggregatedReporter CreateReporter(IEnumerable> steps) + => new AggregateTestReporterSimple(InternalReporter, steps); + + protected override ITestableAggregatedReporter CreateReporterWithComparer(IEnumerable> steps) + => new AggregateTestReporterSimple(InternalReporter, steps, new TestProgressStepEqualityComparer()); + + public override void Ctor_NullArgs_Throws() + { + Assert.Throws(() => new AggregateTestReporterSimple(null!, [])); + Assert.Throws(() => new AggregateTestReporterSimple(InternalReporter, null!)); + Assert.Throws(() => new AggregateTestReporterSimple(null!, [], EqualityComparer>.Default)); + Assert.Throws(() => new AggregateTestReporterSimple(InternalReporter, null!, EqualityComparer>.Default)); + Assert.Throws(() => new AggregateTestReporterSimple(InternalReporter, [], null!)); } +} + +#endregion + +#region Base Test Class + +public abstract class AggregatedProgressReporterTestBase : TestBaseWithServiceProvider where T : ITestInfo, new() +{ + protected readonly TestProgressReporter InternalReporter = new(); + + protected abstract T CreateCustomProgressInfo(TestProgressStep step, double progress); + protected abstract ITestableAggregatedReporter CreateReporter(IEnumerable> steps); + protected abstract ITestableAggregatedReporter CreateReporterWithComparer(IEnumerable> steps); + + [Fact] + public abstract void Ctor_NullArgs_Throws(); [Fact] public void Ctor_SetsProperties() @@ -55,22 +121,21 @@ public void Ctor_SetsProperties() var steps = new List> { step, - step, // Add same reference twice + step, new(2, "Step 2", ServiceProvider), new(3, "Step 3", ServiceProvider) }; - using var reporter = new AggregateTestReporter(_internalReporter, steps); + using var reporter = CreateReporter(steps); Assert.Equal(6, reporter.TotalSize); Assert.Equal(3, reporter.TotalStepCount); } - [Fact] public void Ctor_SetsProperties_EmptySteps() { - using var reporter = new AggregateTestReporter(_internalReporter, []); + using var reporter = CreateReporter([]); Assert.Equal(0, reporter.TotalSize); Assert.Equal(0, reporter.TotalStepCount); @@ -84,12 +149,12 @@ public void Ctor_SetsProperties_WithEqualityComparer() var steps = new List> { step, - other, // Add a step that equals 'step' + other, new(2, "Step 2", ServiceProvider), new(3, "Step 3", ServiceProvider) }; - using var reporter = new AggregateTestReporter(_internalReporter, steps, new TestStepEqualityComparer()); + using var reporter = CreateReporterWithComparer(steps); Assert.Equal(6, reporter.TotalSize); Assert.Equal(3, reporter.TotalStepCount); @@ -99,45 +164,45 @@ public void Ctor_SetsProperties_WithEqualityComparer() public void Report_IgnoresUnregisteredStep() { var step = new TestProgressStep(1, "Step 1", ServiceProvider); - _ = new AggregateTestReporter(_internalReporter, []); - step.Report( 0.5, "step", CreateCustomProgressInfo(step, 0.5)); - Assert.Null(_internalReporter.ReportedData); + _ = CreateReporter([]); + step.Report(0.5, "step", CreateCustomProgressInfo(step, 0.5)); + Assert.Null(InternalReporter.ReportedData); } [Fact] public void Report_DefaultT() { var step = new TestProgressStep(1, "Step 1", ServiceProvider); - _ = new AggregateTestReporter(_internalReporter, [step]); - step.Report( 0.5, "Text", default); - - Assert.NotNull(_internalReporter.ReportedData); - Assert.Equal("Step 1aggregated", _internalReporter.ReportedData.Text); - Assert.Equal("test", _internalReporter.ReportedData.Type.Id); - Assert.Equal(0.5, _internalReporter.ReportedData.Progress); - if (typeof(T).IsValueType) - Assert.Equal(0, _internalReporter.ReportedData.ProgressInfo!.Progress); + var r = CreateReporter([step]); + step.Report(0.5, "Text", default); + + Assert.NotNull(InternalReporter.ReportedData); + Assert.Equal("Step 1aggregated", InternalReporter.ReportedData.Text); + Assert.Equal("test", InternalReporter.ReportedData.Type.Id); + Assert.Equal(0.5, InternalReporter.ReportedData.Progress); + if (typeof(T).IsValueType) + Assert.Equal(0, InternalReporter.ReportedData.ProgressInfo!.Progress); else - Assert.Equal(-1, _internalReporter.ReportedData.ProgressInfo!.Progress); - Assert.True(_internalReporter.ReportedData.ProgressInfo!.Aggregated); + Assert.Equal(-1, InternalReporter.ReportedData.ProgressInfo!.Progress); + Assert.True(InternalReporter.ReportedData.ProgressInfo!.Aggregated); } [Fact] public void Report_DefaultCustomT() { var step = new TestProgressStep(1, "Step 1", ServiceProvider); - _ = new AggregateTestReporter(_internalReporter, [step]); + _ = CreateReporter([step]); step.Report(0.5, "Text", CreateCustomProgressInfo(step, 0.5)); var expected = CreateCustomProgressInfo(step, 0.5); expected.Aggregated = true; - Assert.NotNull(_internalReporter.ReportedData); - Assert.Equal("Step 1aggregated", _internalReporter.ReportedData.Text); - Assert.Equal("test", _internalReporter.ReportedData.Type.Id); - Assert.Equal(0.5, _internalReporter.ReportedData.Progress); - Assert.Equal(expected, _internalReporter.ReportedData.ProgressInfo); + Assert.NotNull(InternalReporter.ReportedData); + Assert.Equal("Step 1aggregated", InternalReporter.ReportedData.Text); + Assert.Equal("test", InternalReporter.ReportedData.Type.Id); + Assert.Equal(0.5, InternalReporter.ReportedData.Progress); + Assert.Equal(expected, InternalReporter.ReportedData.ProgressInfo); } [Fact] @@ -146,72 +211,89 @@ public void Report() var step1 = new TestProgressStep(1, "Step 1", ServiceProvider); var step2 = new TestProgressStep(1, "Step 2", ServiceProvider); - _ = new AggregateTestReporter(_internalReporter, [step1, step2]); + _ = CreateReporter([step1, step2]); step1.Report(0.5, "step1", default); - Assert.NotNull(_internalReporter.ReportedData); - - Assert.Equal("Step 1aggregated", _internalReporter.ReportedData.Text); - Assert.Equal("test", _internalReporter.ReportedData.Type.Id); - Assert.Equal(0.5, _internalReporter.ReportedData.Progress); + Assert.NotNull(InternalReporter.ReportedData); + Assert.Equal("Step 1aggregated", InternalReporter.ReportedData.Text); + Assert.Equal("test", InternalReporter.ReportedData.Type.Id); + Assert.Equal(0.5, InternalReporter.ReportedData.Progress); if (typeof(T).IsValueType) - Assert.Equal(0, _internalReporter.ReportedData.ProgressInfo!.Progress); + Assert.Equal(0, InternalReporter.ReportedData.ProgressInfo!.Progress); else - Assert.Equal(-1, _internalReporter.ReportedData.ProgressInfo!.Progress); - Assert.True(_internalReporter.ReportedData.ProgressInfo!.Aggregated); + Assert.Equal(-1, InternalReporter.ReportedData.ProgressInfo!.Progress); + Assert.True(InternalReporter.ReportedData.ProgressInfo!.Aggregated); - step2.Report( 1, null, default); + step2.Report(1, null, default); - Assert.Equal("Step 2aggregated", _internalReporter.ReportedData.Text); - Assert.Equal("test", _internalReporter.ReportedData.Type.Id); - Assert.Equal(1, _internalReporter.ReportedData.Progress); + Assert.Equal("Step 2aggregated", InternalReporter.ReportedData.Text); + Assert.Equal("test", InternalReporter.ReportedData.Type.Id); + Assert.Equal(1, InternalReporter.ReportedData.Progress); if (typeof(T).IsValueType) - Assert.Equal(0, _internalReporter.ReportedData.ProgressInfo!.Progress); + Assert.Equal(0, InternalReporter.ReportedData.ProgressInfo!.Progress); else - Assert.Equal(-1, _internalReporter.ReportedData.ProgressInfo!.Progress); - Assert.True(_internalReporter.ReportedData.ProgressInfo!.Aggregated); + Assert.Equal(-1, InternalReporter.ReportedData.ProgressInfo!.Progress); + Assert.True(InternalReporter.ReportedData.ProgressInfo!.Aggregated); } [Fact] public void Report_NoReportIfDisposed() { var step = new TestProgressStep(1, "Step 1", ServiceProvider); - var aggregator = new AggregateTestReporter(_internalReporter, [step]); + var aggregator = CreateReporter([step]); aggregator.Dispose(); - step.Report( 0.5, "step", CreateCustomProgressInfo(step, 0.5)); - Assert.Null(_internalReporter.ReportedData); + step.Report(0.5, "step", CreateCustomProgressInfo(step, 0.5)); + Assert.Null(InternalReporter.ReportedData); } } -internal class TestStepEqualityComparer : EqualityComparer> +#endregion + +#region Test Infrastructure + +public interface ITestableAggregatedReporter : IDisposable +{ + long TotalSize { get; } + int TotalStepCount { get; } +} + +public class TestStepEqualityComparer : EqualityComparer> { public override bool Equals(TestProgressStep? x, TestProgressStep? y) { - if (ReferenceEquals(x, y)) - return true; - if (x is null || y is null) - return false; + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; return x.Text.Equals(y.Text); } - public override int GetHashCode(TestProgressStep obj) + public override int GetHashCode(TestProgressStep obj) => obj.Text.GetHashCode(); +} + +public class TestProgressStepEqualityComparer : EqualityComparer> +{ + public override bool Equals(IProgressStep? x, IProgressStep? y) { - return obj.Text.GetHashCode(); + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + return x is TestProgressStep tx && y is TestProgressStep ty && tx.Text.Equals(ty.Text); } + + public override int GetHashCode(IProgressStep obj) => + obj is TestProgressStep t ? t.Text.GetHashCode() : obj.GetHashCode(); } -internal class AggregateTestReporter : AggregatedProgressReporter, T> where T : ITestInfo, new() +public class AggregateTestReporter : AggregatedProgressReporter, T>, ITestableAggregatedReporter + where T : ITestInfo, new() { + long ITestableAggregatedReporter.TotalSize => TotalSize; + int ITestableAggregatedReporter.TotalStepCount => TotalStepCount; + public AggregateTestReporter(IProgressReporter progressReporter, IEnumerable> steps) - : base(progressReporter, steps) - { - } + : base(progressReporter, steps) { } public AggregateTestReporter(IProgressReporter progressReporter, IEnumerable> steps, IEqualityComparer> equalityComparer) - : base(progressReporter, steps, equalityComparer) - { - } + : base(progressReporter, steps, equalityComparer) { } protected override string GetProgressText(TestProgressStep step, string? progressText) { @@ -221,16 +303,37 @@ protected override string GetProgressText(TestProgressStep step, string? prog protected override ProgressEventArgs CalculateAggregatedProgress(TestProgressStep task, ProgressEventArgs progress) { - var newT = new T - { - Aggregated = true, - Progress = progress.ProgressInfo?.Progress ?? -1 - }; + var newT = new T { Aggregated = true, Progress = progress.ProgressInfo?.Progress ?? -1 }; + return new ProgressEventArgs(progress.Progress, "aggregated", newT); + } +} + +public class AggregateTestReporterSimple : AggregatedProgressReporter, ITestableAggregatedReporter + where T : ITestInfo, new() +{ + long ITestableAggregatedReporter.TotalSize => TotalSize; + int ITestableAggregatedReporter.TotalStepCount => TotalStepCount; + + public AggregateTestReporterSimple(IProgressReporter progressReporter, IEnumerable> steps) + : base(progressReporter, steps) { } + + public AggregateTestReporterSimple(IProgressReporter progressReporter, IEnumerable> steps, IEqualityComparer> equalityComparer) + : base(progressReporter, steps, equalityComparer) { } + + protected override string GetProgressText(IProgressStep step, string? progressText) + { + Assert.Equal("aggregated", progressText); + return ((TestProgressStep)step).Text + progressText; + } + + protected override ProgressEventArgs CalculateAggregatedProgress(IProgressStep task, ProgressEventArgs progress) + { + var newT = new T { Aggregated = true, Progress = progress.ProgressInfo?.Progress ?? -1 }; return new ProgressEventArgs(progress.Progress, "aggregated", newT); } } -internal class TestProgressReporter : IProgressReporter +public class TestProgressReporter : IProgressReporter { public ReportedData? ReportedData { get; private set; } @@ -246,10 +349,12 @@ public void Report(double progress, string? progressText, ProgressType type, T? } } -internal class ReportedData +public class ReportedData { public string? Text { get; init; } public double Progress { get; init; } public ProgressType Type { get; init; } public T? ProgressInfo { get; init; } -} \ No newline at end of file +} + +#endregion \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Progress/ProgressEventArgsTest.cs b/src/CommonUtilities.SimplePipeline/test/Progress/ProgressEventArgsTest.cs index 2540a22..ed3cf7d 100644 --- a/src/CommonUtilities.SimplePipeline/test/Progress/ProgressEventArgsTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Progress/ProgressEventArgsTest.cs @@ -1,5 +1,6 @@ using System; using AnakinRaW.CommonUtilities.SimplePipeline.Progress; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Progress; diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/AsyncStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/AsyncStepRunnerTest.cs new file mode 100644 index 0000000..4b691f5 --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Runners/AsyncStepRunnerTest.cs @@ -0,0 +1,21 @@ +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; + +public class AsyncStepRunnerTest : StepRunnerTestBase +{ + public override bool HasSequentialStepExecutionOrder => false; + + public override bool SupportsSequentialExecutionOrder => true; + + protected override AsyncStepRunner CreateStepRunner(bool? sequential = null) + { + var workers = sequential is true ? 1 : 4; + return CreateStepRunner(workers); + } + + protected override AsyncStepRunner CreateStepRunner(int workerCount) + { + return new AsyncStepRunner(workerCount, ServiceProvider); + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerStepRunnerTest.cs deleted file mode 100644 index 593ebec..0000000 --- a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelProducerConsumerStepRunnerTest.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.SimplePipeline.Runners; -using Xunit; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; - -public class ParallelProducerConsumerStepRunnerTest : ParallelStepRunnerTestBase -{ - protected override ParallelProducerConsumerStepRunner CreateParallelRunner(int workerCount = 2) - { - return CreateConsumerStepRunner(workerCount); - } - - private ParallelProducerConsumerStepRunner CreateConsumerStepRunner(int workerCount = 2) - { - return new ParallelProducerConsumerStepRunner(workerCount, ServiceProvider); - } - - protected override void FinishAdding(ParallelProducerConsumerStepRunner runner) - { - runner.Finish(); - } - - [Fact] - public async Task Run_WaitNotFinished() - { - var runner = CreateStepRunner(); - - var tsc1 = new TaskCompletionSource(); - var tsc2 = new TaskCompletionSource(); - - var s1 = new TestStep(_ => tsc1.SetResult(1), ServiceProvider); - var s2 = new TestStep(_ => tsc2.SetResult(1), ServiceProvider); - - runner.AddStep(s1); - runner.AddStep(s2); - - _ = runner.RunAsync(CancellationToken.None); - - await tsc1.Task; - await tsc1.Task; - - Assert.Throws(() => runner.Wait(TimeSpan.FromSeconds(2))); - } - - - [Fact] - public async Task Run_WaitNotFinished_CancellationShouldFinish() - { - var runner = CreateStepRunner(); - - var cts = new CancellationTokenSource(); - var task = runner.RunAsync(cts.Token); - - // Give it some time - await Task.Delay(500, CancellationToken.None); - - cts.Cancel(); - - await task; - - Assert.True(runner.IsCancelled); - Assert.NotNull(runner.Exception); - Assert.IsType(runner.Exception.InnerExceptions.First(), true); - } - - [Fact] - public void Run_AddDelayed() - { - var runner = CreateStepRunner(); - - var ran1 = false; - var ran2 = false; - var ran3 = false; - var s1 = new TestStep(_ => ran1 = true, ServiceProvider); - var s2 = new TestStep(_ => ran2 = true, ServiceProvider); - var s3 = new TestStep(_ => ran3 = true, ServiceProvider); - - runner.AddStep(s1); - runner.AddStep(s2); - - _ = runner.RunAsync(CancellationToken.None); - - Task.Run(() => - { - runner.AddStep(s3); - Task.Delay(1000); - runner.Finish(); - }, TestContext.Current.CancellationToken); - - runner.Wait(); - - Assert.True(ran1); - Assert.True(ran2); - Assert.True(ran3); - } - - [Fact] - public async Task Run_AddDelayed_Await() - { - var runner = CreateStepRunner(); - - var ran1 = false; - var ran2 = false; - var ran3 = false; - var s1 = new TestStep(_ => ran1 = true, ServiceProvider); - var s2 = new TestStep(_ => ran2 = true, ServiceProvider); - var s3 = new TestStep(_ => ran3 = true, ServiceProvider); - - runner.AddStep(s1); - runner.AddStep(s2); - - var runTask = runner.RunAsync(CancellationToken.None); - - Task.Run(() => - { - runner.AddStep(s3); - runner.Finish(); - - }, TestContext.Current.CancellationToken).Forget(); - - await runTask; - // Should not block - runner.Wait(); - - Assert.True(ran1); - Assert.True(ran2); - Assert.True(ran3); - } - - [Fact] - public async Task Run_AddDelayed_Cancelled() - { - var runner = CreateStepRunner(); - - var tcs = new TaskCompletionSource(); - - var ran1 = false; - var s1 = new TestStep(_ => - { - ran1 = true; - tcs.SetResult(0); - }, ServiceProvider); - var ran2 = false; - var s2 = new TestStep(_ => ran2 = true, ServiceProvider); - - runner.AddStep(s1); - - var cts = new CancellationTokenSource(); - - var runTask = runner.RunAsync(cts.Token); - - Task.Run(async () => - { - await tcs.Task.ConfigureAwait(false); - await Task.Delay(1000, CancellationToken.None); // Give it some time, so ensure the runner is internally blocking and waiting for the next step. - cts.Cancel(); - runner.AddStep(s2); - }, CancellationToken.None).Forget(); - - - await runTask; - - Assert.True(ran1); - Assert.False(ran2); - Assert.Equal([s1], runner.ExecutedSteps); - - Assert.True(runner.IsCancelled); - Assert.NotNull(runner.Exception); - - Assert.IsType(runner.Exception.InnerExceptions.First(), true); - } - - [Fact] - public void AddStep_AddAfterFinish() - { - var runner = CreateStepRunner(); - var s1 = new TestStep(_ => { }, ServiceProvider); - runner.AddStep(s1); - - runner.Finish(); - - Assert.Throws(() => runner.AddStep(s1)); - } - - [Fact] - public async Task RunAsync_Cancelled() - { - // Have deterministic result - var runner = CreateParallelRunner(1); - - var cts = new CancellationTokenSource(); - - var b = new ManualResetEvent(false); - - var step1 = new TestStep(_ => - { - cts.Cancel(); - b.Set(); - }, ServiceProvider); - - var step2 = new TestStep(_ => {}, ServiceProvider); - - runner.AddStep(step1); - runner.AddStep(step2); - - FinishAdding(runner); - - await runner.RunAsync(cts.Token); - - // This is all we can test for, because we cannot know whether the cancellation was requested while fetching step queue data - // or the step was already fetched - Assert.True(runner.IsCancelled); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTest.cs deleted file mode 100644 index 3c56fdd..0000000 --- a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTest.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Threading; -using AnakinRaW.CommonUtilities.SimplePipeline.Runners; -using Xunit; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; - -public class ParallelStepRunnerTest : ParallelStepRunnerTestBase -{ - protected override ParallelStepRunner CreateParallelRunner(int workerCount = 2) - { - return CreateParallelStepRunner(workerCount); - } - - private ParallelStepRunner CreateParallelStepRunner(int workerCount = 2) - { - return new ParallelStepRunner(workerCount, ServiceProvider); - } - - [Fact] - public void Ctor_InvalidArgs_Throws() - { - Assert.Throws(() => new ParallelStepRunner(1, null!)); - Assert.Throws(() => new ParallelStepRunner(new Random().Next(int.MinValue, 0), ServiceProvider)); - Assert.Throws(() => new ParallelStepRunner(0, ServiceProvider)); - } - - [Fact] - public async Task RunAsync_Cancelled() - { - // Have deterministic result - var runner = CreateParallelRunner(1); - - var cts = new CancellationTokenSource(); - - var b = new ManualResetEvent(false); - - StepRunnerErrorEventArgs? error = null; - runner.Error += (_, e) => - { - error = e; - }; - - var ran1 = false; - var step1 = new TestStep(_ => - { - ran1 = true; - cts.Cancel(); - b.Set(); - }, ServiceProvider); - - var ran2 = false; - var step2 = new TestStep(_ => ran2 = true, ServiceProvider); - - runner.AddStep(step1); - runner.AddStep(step2); - - FinishAdding(runner); - - await runner.RunAsync(cts.Token); - - Assert.True(runner.IsCancelled); - Assert.NotNull(error); - Assert.True(error.Cancel); - Assert.True(ran1); - Assert.False(ran2); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTestBase.cs deleted file mode 100644 index e446301..0000000 --- a/src/CommonUtilities.SimplePipeline/test/Runners/ParallelStepRunnerTestBase.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; - -public abstract class ParallelStepRunnerTestBase : StepRunnerTestBase where T : class, IParallelStepRunner -{ - public override bool PreservesStepExecutionOrder => false; - - protected abstract T CreateParallelRunner(int workerCount = 2); - - protected sealed override T CreateStepRunner(bool deterministic = false) - { - return CreateParallelRunner(deterministic ? 1 : new Random().Next(2, 6)); - } - - [Fact] - public void Ctor_WorkerCount() - { - Assert.Throws(() => CreateParallelRunner(new Random().Next(65, int.MaxValue))); - Assert.Throws(() => CreateParallelRunner(new Random().Next(int.MinValue, 0))); - Assert.Throws(() => CreateParallelRunner(0)); - - var numRunners = new Random().Next(1, 64); - var runner = CreateParallelRunner(numRunners); - - Assert.Equal(numRunners, runner.WorkerCount); - } - - [Fact] - public void Wait() - { - var runner = CreateParallelRunner(); - - var ran1 = false; - var ran2 = false; - - var step1 = new TestStep(_ => ran1 = true, ServiceProvider); - var step2 = new TestStep(_ => ran2 = true, ServiceProvider); - - runner.AddStep(step1); - runner.AddStep(step2); - - FinishAdding(runner); - - runner.RunAsync(CancellationToken.None).Forget(); - runner.Wait(); - - // We cannot assert on the returned task, - // as the impl. creates different tasks for await and Wait(). - // This may result in a race where the Wait() reports completion before the awaitable task - - Assert.True(ran1); - Assert.True(ran2); - } - - [Fact] - public void Wait_FailedRunner_Throws() - { - var runner = CreateParallelRunner(); - - var ran1 = false; - var ran2 = false; - - var step1 = new TestStep(_ => - { - ran1 = true; - throw new Exception("Test"); - }, ServiceProvider); - var step2 = new TestStep(_ => ran2 = true, ServiceProvider); - - runner.AddStep(step1); - runner.AddStep(step2); - - FinishAdding(runner); - - var runnerTask = runner.RunAsync(CancellationToken.None); - - var e = Assert.Throws(() => runner.Wait()); - Assert.Equal("Test", e.InnerExceptions.First().Message); - - Assert.True(runnerTask.IsCompleted); - Assert.False(runnerTask.IsFaulted); - - Assert.NotNull(runner.Exception); - - Assert.True(ran1); - Assert.True(ran2); - } - - [Fact] - public async Task RunAsync_Await() - { - var runner = CreateParallelRunner(); - - var b = new ManualResetEvent(false); - - var ran1 = false; - var ran2 = false; - - var step1 = new TestStep(_ => - { - b.WaitOne(); - ran1 = true; - }, ServiceProvider); - var step2 = new TestStep(_ => - { - b.WaitOne(); - ran2 = true; - }, ServiceProvider); - - runner.AddStep(step1); - runner.AddStep(step2); - - FinishAdding(runner); - - var runTask = runner.RunAsync(CancellationToken.None); - - Assert.False(ran1); - Assert.False(ran2); - - b.Set(); - - await runTask; - - Assert.True(ran1); - Assert.True(ran2); - - Assert.Equivalent(new HashSet([step1, step2]), runner.ExecutedSteps, true); - } - - [Fact] - public async Task Wait_Timeout_ThrowsTimeoutException() - { - var runner = CreateParallelRunner(); - - var b = new ManualResetEvent(false); - - var step1 = new TestStep(_ => - { - b.WaitOne(); - }, ServiceProvider); - - runner.AddStep(step1); - - FinishAdding(runner); - - var runnerTask = runner.RunAsync(CancellationToken.None); - - Assert.Throws(() => runner.Wait(TimeSpan.FromMilliseconds(100))); - - // Even if Wait throws, we still continue executing - b.Set(); - await runnerTask; - - Assert.True(runnerTask.IsCompleted); - } - - [Fact] - public async Task RunAsync_ErrorSetsCancellation() - { - // Have deterministic result - var runner = CreateParallelRunner(1); - - var step1 = new TestStep(_ => throw new StopRunnerException(), ServiceProvider); - var ran2 = false; - var step2 = new TestStep(_ => { ran2 = true; }, ServiceProvider); - - runner.AddStep(step1); - runner.AddStep(step2); - - // Do not signal runner to finish. It must do that on itself! - - var runnerTask = runner.RunAsync(CancellationToken.None); - - await runnerTask; - - Assert.False(ran2); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/ProducerConsumerStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/ProducerConsumerStepRunnerTest.cs new file mode 100644 index 0000000..d75912e --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Runners/ProducerConsumerStepRunnerTest.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; +using Xunit; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; + +public class ProducerConsumerStepRunnerTest : StepRunnerTestBase +{ + public override bool HasSequentialStepExecutionOrder => false; + + public override bool SupportsSequentialExecutionOrder => true; + + protected override bool SupportsAddingStepsAfterCancellation => false; + + protected override ProducerConsumerStepRunner CreateStepRunner(bool? sequential = null) + { + var workers = sequential is true ? 1 : 4; + return CreateStepRunner(workers); + } + + protected override ProducerConsumerStepRunner CreateStepRunner(int workerCount) + { + return new ProducerConsumerStepRunner(workerCount, ServiceProvider); + } + + protected override void FinishAdding(ProducerConsumerStepRunner runner) + { + base.FinishAdding(runner); + runner.Finish(); + } + + #region Finish + + [Fact] + public void Finish_CalledMultipleTimes() + { + var runner = CreateStepRunner(); + runner.Finish(); + runner.Finish(); + runner.Finish(); + } + + [Fact] + public async Task Finish_CalledBeforeRun_AllStepsExecute() + { + var runner = CreateStepRunner(); + var executedSteps = new ConcurrentBag(); + + for (var i = 0; i < 10; i++) + { + var index = i; + runner.AddStep(new TestStep(_ => + { + executedSteps.Add(index); + return Task.CompletedTask; + }, ServiceProvider)); + } + + runner.Finish(); + await runner.RunAsync(CancellationToken.None); + + Assert.Equal(10, executedSteps.Count); + } + + [Fact] + public void Finish_AddStepAfterFinish_ThrowsException() + { + var runner = CreateStepRunner(); + runner.Finish(); + + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + Assert.Throws(() => runner.AddStep(step)); + } + + [Fact] + public async Task Finish_WhileRunning_AllQueuedStepsComplete() + { + var runner = CreateStepRunner(workerCount: 2); + var executedSteps = new ConcurrentBag(); + var step1Started = new ManualResetEventSlim(false); + var allStepsAdded = new ManualResetEventSlim(false); + + runner.AddStep(new TestStep(async _ => + { + await Task.Yield(); + step1Started.Set(); + executedSteps.Add(1); + allStepsAdded.Wait(TestContext.Current.CancellationToken); + }, ServiceProvider)); + + var runTask = runner.RunAsync(CancellationToken.None); + + step1Started.Wait(TestContext.Current.CancellationToken); + + for (var i = 2; i <= 5; i++) + { + var index = i; + runner.AddStep(new TestStep(_ => + { + executedSteps.Add(index); + return Task.CompletedTask; + }, ServiceProvider)); + } + allStepsAdded.Set(); + + runner.Finish(); + await runTask; + + Assert.Equal(5, executedSteps.Count); + } + + #endregion + + #region As Sequential + + [Fact] + public async Task Sequential_TakeNextStep_BlocksUntilStepAvailable() + { + var runner = CreateStepRunner(workerCount: 1); + var step1Started = new ManualResetEventSlim(false); + var step2Added = new ManualResetEventSlim(false); + var executedSteps = new ConcurrentBag(); + + runner.AddStep(new TestStep(async _ => + { + await Task.Yield(); + step1Started.Set(); + executedSteps.Add(1); + step2Added.Wait(TestContext.Current.CancellationToken); + }, ServiceProvider)); + + var runTask = runner.RunAsync(CancellationToken.None); + + step1Started.Wait(TestContext.Current.CancellationToken); + + runner.AddStep(new TestStep(_ => + { + executedSteps.Add(2); + return Task.CompletedTask; + }, ServiceProvider)); + + step2Added.Set(); + runner.Finish(); + await runTask; + + Assert.Equal(2, executedSteps.Count); + Assert.Contains(1, executedSteps); + Assert.Contains(2, executedSteps); + } + + [Fact] + public async Task Error_SetCancelToTrue_Sequential_StepsInQueue_AreNotExecuted() + { + var runner = CreateStepRunner(workerCount: 1); + var executedSteps = new ConcurrentBag(); + var errorOccurred = new ManualResetEventSlim(false); + var barrier = new ManualResetEventSlim(false); + + runner.Error += (_, args) => + { + args.Cancel = true; + errorOccurred.Set(); + }; + + runner.AddStep(new TestStep(async _ => + { + await Task.Yield(); + executedSteps.Add(1); + barrier.Wait(TestContext.Current.CancellationToken); + throw new InvalidOperationException("Test error"); + }, ServiceProvider)); + + for (var i = 2; i <= 5; i++) + { + var index = i; + runner.AddStep(new TestStep(_ => + { + executedSteps.Add(index); + return Task.CompletedTask; + }, ServiceProvider)); + } + + runner.Finish(); + var runTask = runner.RunAsync(CancellationToken.None); + + await Task.Delay(100, TestContext.Current.CancellationToken); + barrier.Set(); + + errorOccurred.Wait(TestContext.Current.CancellationToken); + await runTask; + + Assert.Contains(1, executedSteps); + Assert.DoesNotContain(2, executedSteps); + Assert.DoesNotContain(3, executedSteps); + Assert.DoesNotContain(4, executedSteps); + Assert.DoesNotContain(5, executedSteps); + } + + #endregion + + #region RunAsync Extended Behavior + + [Fact] + public async Task RunAsync_MultipleWorkers_ExecutesStepsConcurrently() + { + const int workerCount = 4; + const int totalSteps = 20; + var runner = CreateStepRunner(workerCount); + var executedSteps = new ConcurrentBag(); + + var concurrentCount = 0; + var maxConcurrentCount = 0; + var lockObj = new object(); + + for (var i = 0; i < totalSteps; i++) + { + var index = i; + runner.AddStep(new TestStep(async _ => + { + var current = Interlocked.Increment(ref concurrentCount); + lock (lockObj) + { + if (current > maxConcurrentCount) + maxConcurrentCount = current; + } + + await Task.Delay(new Random().Next(50, 300), TestContext.Current.CancellationToken); + executedSteps.Add(index); + + Interlocked.Decrement(ref concurrentCount); + }, ServiceProvider)); + } + + runner.Finish(); + await runner.RunAsync(CancellationToken.None); + + Assert.Equal(totalSteps, executedSteps.Count); + Assert.True(maxConcurrentCount >= 2, $"Expected concurrent execution, but max concurrent was {maxConcurrentCount}"); + } + + [Fact] + public async Task RunAsync_NotFinished_NeverEnds() + { + var runner = CreateStepRunner(); + + var tsc1 = new TaskCompletionSource(); + var tsc2 = new TaskCompletionSource(); + + var s1 = new TestStep(_ => { tsc1.SetResult(1); return Task.CompletedTask; }, ServiceProvider); + var s2 = new TestStep(_ => { tsc2.SetResult(1); return Task.CompletedTask; }, ServiceProvider); + + runner.AddStep(s1); + runner.AddStep(s2); + + _ = runner.RunAsync(CancellationToken.None); + + await tsc1.Task; + await tsc2.Task; + + Assert.Throws(() => runner.Wait(TimeSpan.FromSeconds(2))); + } + + [Fact] + public async Task RunAsync_AddStep_AfterCancellation() + { + var runner = CreateStepRunner(); + + StepRunnerErrorEventArgs? raisedArgs = null; + runner.Error += (_, args) => + { + raisedArgs = args; + Assert.Null(args.Step); + Assert.True(args.Cancel); + }; + + var tcs = new TaskCompletionSource(); + + var ran1 = false; + var s1 = new TestStep(async _ => + { + await Task.Yield(); + ran1 = true; + tcs.SetResult(0); + }, ServiceProvider); + + runner.AddStep(s1); + + var cts = new CancellationTokenSource(); + + var runTask = runner.RunAsync(cts.Token); + + Task.Run(async () => + { + await tcs.Task.ConfigureAwait(false); + + // Give it some time, so ensure the runner is internally blocking and waiting for the next step. + await Task.Delay(1000, CancellationToken.None); + cts.Cancel(); + Assert.Throws(() => runner.AddStep(new TestStep(_ => Task.CompletedTask, ServiceProvider))); + }, CancellationToken.None).Forget(); + + + await runTask; + + Assert.True(ran1); + Assert.Equal([s1], runner.ExecutedSteps); + + Assert.True(runner.IsCancelled); + Assert.Null(runner.Exception); + Assert.NotNull(raisedArgs); + } + + #endregion + + #region Automatic Finish + + [Fact] + public async Task Finished_OnStopRunnerException_RunnerFinishes() + { + var runner = CreateStepRunner(workerCount: 4); + + StepRunnerErrorEventArgs? raisedArgs = null; + runner.Error += (_, args) => + { + Assert.True(args.Cancel); + raisedArgs = args; + }; + + runner.AddStep(new TestStep(_ => throw new StopRunnerException(), ServiceProvider)); + + await runner.RunAsync(CancellationToken.None); + + Assert.NotNull(raisedArgs); + + var step2 = new TestStep(_ => Task.CompletedTask, ServiceProvider); + Assert.Throws(() => runner.AddStep(step2)); + } + + [Fact] + public async Task Finished_CancellationShouldFinish() + { + var runner = CreateStepRunner(); + + StepRunnerErrorEventArgs? raisedArgs = null; + runner.Error += (_, args) => + { + raisedArgs = args; + Assert.Null(args.Step); + Assert.True(args.Cancel); + }; + + var cts = new CancellationTokenSource(); + var task = runner.RunAsync(cts.Token); + + // Give it some time + await Task.Delay(500, CancellationToken.None); + + cts.Cancel(); + + await task; + + Assert.Throws(() => runner.AddStep(new TestStep(_ => Task.CompletedTask, ServiceProvider))); + + Assert.True(runner.IsCancelled); + Assert.Null(runner.Exception); + Assert.NotNull(raisedArgs); + } + + #endregion +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/SequentialStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/SequentialStepRunnerTest.cs index 708b73a..6449546 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/SequentialStepRunnerTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/SequentialStepRunnerTest.cs @@ -1,20 +1,27 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Runners; using System; -using System.Threading.Tasks; -using System.Threading; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; public class SequentialStepRunnerTest : StepRunnerTestBase { - public override bool PreservesStepExecutionOrder => true; + public override bool HasSequentialStepExecutionOrder => true; - protected override SequentialStepRunner CreateStepRunner(bool deterministic = false) + public override bool SupportsSequentialExecutionOrder => true; + + protected override SequentialStepRunner CreateStepRunner(bool? sequential = null) { + if (sequential is false) + throw new InvalidOperationException(); return new SequentialStepRunner(ServiceProvider); } + protected override SequentialStepRunner CreateStepRunner(int workerCount) + { + throw new NotSupportedException(); + } + [Fact] public void Ctor_InvalidArgs_Throws() { @@ -22,30 +29,9 @@ public void Ctor_InvalidArgs_Throws() } [Fact] - public async Task RunAsync_ErrorSetsCancellation() + public void Ctor_WorkerCountIsOne() { - var runner = CreateStepRunner(); - - var errorCounter = 0; - runner.Error += (_, e) => - { - errorCounter++; - if (e.Step?.Error?.Message == "Test") - e.Cancel = true; - }; - - var step1 = new TestStep(_ => throw new Exception("Test"), ServiceProvider); - var ran2 = false; - var step2 = new TestStep(_ => { ran2 = true; }, ServiceProvider); - - runner.AddStep(step1); - runner.AddStep(step2); - - var runnerTask = runner.RunAsync(CancellationToken.None); - - await runnerTask; - - Assert.Equal(2, errorCounter); - Assert.False(ran2); + var runner = new SequentialStepRunner(ServiceProvider); + Assert.Equal(1, runner.WorkerCount); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs index 6cca680..ad95334 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs @@ -1,214 +1,2090 @@ -using System; +using AnakinRaW.CommonUtilities.Testing; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; -public abstract class StepRunnerTestBase : TestBaseWithServiceProvider where T : class, IStepRunner +public abstract class StepRunnerTestBase : TestBaseWithServiceProvider where T : AsyncStepRunner { - public abstract bool PreservesStepExecutionOrder { get; } + /// + /// Indicates whether the runner guarantees sequential step execution order. + /// + public virtual bool HasSequentialStepExecutionOrder => false; - protected abstract T CreateStepRunner(bool deterministic = false); + /// + /// Indicates whether the runner supports sequential execution mode. + /// + public virtual bool SupportsSequentialExecutionOrder => true; + + protected virtual bool SupportsAddingStepsAfterCancellation => true; + + protected abstract T CreateStepRunner(bool? sequential = null); + + protected abstract T CreateStepRunner(int workerCount); protected virtual void FinishAdding(T runner) { } + #region Initial State Tests + + [Fact] + public void NewRunner_InitialState_ConcurrentRunner() + { + if (HasSequentialStepExecutionOrder) + return; + Assert.Throws("workerCount", () => CreateStepRunner(0)); + Assert.Throws("workerCount", () => CreateStepRunner(new Random().Next(int.MinValue, 0))); + Assert.Throws("workerCount", () => CreateStepRunner(new Random().Next(65, int.MaxValue))); + + var workerCount = new Random().Next(2, 65); + var runner = CreateStepRunner(workerCount); + Assert.Equal(workerCount, runner.WorkerCount); + Assert.False(runner.IsSequential); + } + + [Fact] + public void NewRunner_InitialState_Sequential() + { + if (!SupportsSequentialExecutionOrder) + return; + + var runner = CreateStepRunner(true); + Assert.Equal(1, runner.WorkerCount); + Assert.True(runner.IsSequential); + } + + [Fact] + public void NewRunner_InitialState_IsCorrect() + { + var runner = CreateStepRunner(); + + Assert.Empty(runner.ExecutedSteps); + Assert.Null(runner.Exception); + Assert.True(runner.WorkerCount >= 1); + } + + [Fact] + public void NewRunner_Sequential_WorkerCountIsOne() + { + if (!SupportsSequentialExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: true); + Assert.Equal(1, runner.WorkerCount); + } + + #endregion + + #region IsRunning Property Tests + + [Fact] + public void IsRunning_BeforeRun_ReturnsFalse() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + runner.AddStep(step); + + Assert.False(runner.IsRunning); + } + + [Fact] + public async Task IsRunning_DuringExecution_ReturnsTrue() + { + var runner = CreateStepRunner(); + var isRunningDuringExecution = false; + var stepStarted = new ManualResetEventSlim(false); + var canComplete = new ManualResetEventSlim(false); + + var step = new TestStep(async _ => + { + await Task.Yield(); + stepStarted.Set(); + isRunningDuringExecution = runner.IsRunning; + canComplete.Wait(TestContext.Current.CancellationToken); + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var runTask = runner.RunAsync(CancellationToken.None); + + stepStarted.Wait(TestContext.Current.CancellationToken); + Assert.True(runner.IsRunning, "IsRunning should be true during execution"); + + canComplete.Set(); + await runTask; + + Assert.True(isRunningDuringExecution, "IsRunning should have been true inside step"); + } + + [Fact] + public async Task IsRunning_AfterSuccessfulCompletion_ReturnsFalse() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.False(runner.IsRunning); + } + + [Fact] + public async Task IsRunning_AfterExecutionWithErrors_ReturnsFalse() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => throw new InvalidOperationException(), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.False(runner.IsRunning); + } + + [Fact] + public async Task IsRunning_AfterCancellation_ReturnsFalse() + { + var runner = CreateStepRunner(); + var cts = new CancellationTokenSource(); + var stepStarted = new ManualResetEventSlim(false); + var canComplete = new ManualResetEventSlim(false); + + var step = new TestStep(async _ => + { + await Task.Yield(); + stepStarted.Set(); + canComplete.Wait(TestContext.Current.CancellationToken); + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var runTask = runner.RunAsync(cts.Token); + + stepStarted.Wait(TestContext.Current.CancellationToken); + cts.Cancel(); + canComplete.Set(); + + await runTask; + + Assert.False(runner.IsRunning); + } + + #endregion + + #region IsCancelled Property Tests + + [Fact] + public void IsCancelled_BeforeRun_ReturnsFalse() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + runner.AddStep(step); + + Assert.False(runner.IsCancelled); + } + + [Fact] + public async Task IsCancelled_DuringNormalExecution_ReturnsFalse() + { + var runner = CreateStepRunner(); + var isCancelledDuringExecution = false; + var stepStarted = new ManualResetEventSlim(false); + var canComplete = new ManualResetEventSlim(false); + + var step = new TestStep(async _ => + { + await Task.Yield(); + stepStarted.Set(); + isCancelledDuringExecution = runner.IsCancelled; + canComplete.Wait(TestContext.Current.CancellationToken); + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var runTask = runner.RunAsync(CancellationToken.None); + + stepStarted.Wait(TestContext.Current.CancellationToken); + Assert.False(runner.IsCancelled, "IsCancelled should be false during normal execution"); + + canComplete.Set(); + await runTask; + + Assert.False(isCancelledDuringExecution, "IsCancelled should have been false inside step"); + } + + [Fact] + public async Task IsCancelled_AfterNormalCompletion_ReturnsFalse() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.False(runner.IsCancelled); + } + + [Fact] + public async Task IsCancelled_AfterCancellation_ReturnsTrue() + { + var runner = CreateStepRunner(); + var cts = new CancellationTokenSource(); + var stepStarted = new ManualResetEventSlim(false); + var canComplete = new ManualResetEventSlim(false); + + var step = new TestStep(async ct => + { + await Task.Yield(); + stepStarted.Set(); + canComplete.Wait(TestContext.Current.CancellationToken); + ct.ThrowIfCancellationRequested(); + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var runTask = runner.RunAsync(cts.Token); + + stepStarted.Wait(TestContext.Current.CancellationToken); + cts.Cancel(); + canComplete.Set(); + + await runTask; + + Assert.True(runner.IsCancelled); + } + + [Fact] + public async Task IsCancelled_AfterExecutionWithErrors_ReturnsFalse() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => throw new InvalidOperationException(), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.False(runner.IsCancelled, "IsCancelled should be false when errors occur without cancellation"); + Assert.NotNull(runner.Exception); + } + + [Fact] + public async Task IsCancelled_AlreadyCancelledToken_ReturnsTrue() + { + var runner = CreateStepRunner(); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(cts.Token); + + Assert.True(runner.IsCancelled); + } + + [Fact] + public async Task IsCancelled_StopRunnerExceptionWithCancel_ReturnsTrue() + { + var runner = CreateStepRunner(); + + var step = new TestStep(_ => throw new StopRunnerException(), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.True(runner.IsCancelled, "IsCancelled should be true when StopRunnerException causes cancellation"); + } + + [Fact] + public async Task IsCancelled_OnErrorSetsCancellation_ReturnsTrue() + { + var runner = CreateStepRunner(); + + runner.Error += (_, args) => + { + args.Cancel = true; + }; + + var step = new TestStep(_ => throw new Exception(), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.True(runner.IsCancelled, "IsCancelled should be true when StopRunnerException causes cancellation"); + } + + #endregion + + #region AddStep Tests + + [Fact] + public void AddStep_Null_ThrowsArgumentNullException() + { + var runner = CreateStepRunner(); + Assert.Throws(() => runner.AddStep(null!)); + } + + [Fact] + public void AddStep_ValidStep_DoesNotThrow() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + var exception = Record.Exception(() => runner.AddStep(step)); + + Assert.Null(exception); + } + + [Fact] + public void AddStep_MultipleSteps_AllAccepted() + { + var runner = CreateStepRunner(); + + for (var i = 0; i < 10; i++) + { + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + runner.AddStep(step); + } + + // Should not throw + } + + [Fact] + public async Task AddStep_DuringExecution_StepGetsExecuted() + { + var runner = CreateStepRunner(); + var executedSteps = new ConcurrentBag(); + var gate = new ManualResetEventSlim(false); + + var step1 = new TestStep(async _ => + { + executedSteps.Add("Step1"); + await Task.Yield(); + gate.Wait(TestContext.Current.CancellationToken); + }, ServiceProvider); + + runner.AddStep(step1); + + var runTask = runner.RunAsync(CancellationToken.None); + + // Add step while runner is executing + var step2 = new TestStep(_ => + { + executedSteps.Add("Step2"); + return Task.CompletedTask; + }, ServiceProvider); + runner.AddStep(step2); + + gate.Set(); + FinishAdding(runner); + + await step2; + await runTask; + + Assert.Contains("Step1", executedSteps); + Assert.Contains("Step2", executedSteps); + } + + #endregion + + #region RunAsync - Basic Execution Tests + + [Fact] + public async Task RunAsync_NoSteps_CompletesSuccessfully() + { + var runner = CreateStepRunner(); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.Empty(runner.ExecutedSteps); + Assert.Null(runner.Exception); + } + + [Fact] + public async Task RunAsync_SingleStep_ExecutesStep() + { + var runner = CreateStepRunner(); + var executed = false; + var step = new TestStep(_ => + { + executed = true; + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.True(executed); + Assert.Contains(step, runner.ExecutedSteps); + await Assert.Single(runner.ExecutedSteps); + } + + [Fact] + public async Task RunAsync_MultipleSteps_ExecutesAllSteps() + { + var runner = CreateStepRunner(); + var executedSteps = new ConcurrentBag(); + var executionOrder = new List(); + var lockObj = new object(); + + var step1 = new TestStep(_ => + { + lock (lockObj) { executionOrder.Add("Step1"); } + executedSteps.Add("Step1"); + return Task.CompletedTask; + }, ServiceProvider); + + var step2 = new TestStep(_ => + { + lock (lockObj) { executionOrder.Add("Step2"); } + executedSteps.Add("Step2"); + return Task.CompletedTask; + }, ServiceProvider); + + var step3 = new TestStep(_ => + { + lock (lockObj) { executionOrder.Add("Step3"); } + executedSteps.Add("Step3"); + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step1); + runner.AddStep(step2); + runner.AddStep(step3); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.Equal(3, executedSteps.Count); + Assert.Contains("Step1", executedSteps); + Assert.Contains("Step2", executedSteps); + Assert.Contains("Step3", executedSteps); + Assert.Equal(3, runner.ExecutedSteps.Count); + + if (HasSequentialStepExecutionOrder) + { + Assert.Equal(new[] { "Step1", "Step2", "Step3" }, executionOrder); + } + } + + [Fact] + public async Task RunAsync_AsyncStep_WaitsForCompletion() + { + var runner = CreateStepRunner(); + var completed = false; + + var step = new TestStep(async _ => + { + await Task.Delay(100, TestContext.Current.CancellationToken); + completed = true; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.True(completed); + } + + [Fact] + public async Task RunAsync_MixedSyncAndAsyncSteps_ExecutesAll() + { + var runner = CreateStepRunner(); + var executed = new ConcurrentBag(); + + runner.AddStep(new TestStep(_ => { executed.Add(1); return Task.CompletedTask; }, ServiceProvider)); + runner.AddStep(new TestStep(async _ => { await Task.Delay(10, TestContext.Current.CancellationToken); executed.Add(2); }, ServiceProvider)); + runner.AddStep(new TestStep(_ => { executed.Add(3); return Task.CompletedTask; }, ServiceProvider)); + runner.AddStep(new TestStep(async _ => { await Task.Yield(); executed.Add(4); }, ServiceProvider)); + + FinishAdding(runner); + await runner.RunAsync(CancellationToken.None); + + Assert.Equal(4, executed.Count); + } + + [Fact] + public async Task RunAsync_SequentialRunner_ExecutesInAddOrder() + { + if (!SupportsSequentialExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: true); + var executionOrder = new List(); + + for (var i = 0; i < 5; i++) + { + var index = i; + var step = new TestStep(async _ => + { + await Task.Yield(); + executionOrder.Add(index); + }, ServiceProvider); + runner.AddStep(step); + } + + FinishAdding(runner); + await runner.RunAsync(CancellationToken.None); + + Assert.Equal(new[] { 0, 1, 2, 3, 4 }, executionOrder); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RunAsync_StepsAreExecutedOnThreadPool_DoesNotDeadlock(bool sequential) + { + if (sequential && !SupportsSequentialExecutionOrder) + return; + if (!sequential && HasSequentialStepExecutionOrder) + return; + + var waitSource = new TaskCompletionSource(); + + var runner = CreateStepRunner(sequential); + var canComplete = new ManualResetEventSlim(false); + + var step1 = new TestStep(_ => + { + canComplete.Wait(); + waitSource.SetResult(true); + return Task.CompletedTask; + }, ServiceProvider); + var step2 = new TestStep(async _ => + { + await waitSource.Task; + }, ServiceProvider); + + + runner.AddStep(step1); + runner.AddStep(step2); + + FinishAdding(runner); + + var task = runner.RunAsync(CancellationToken.None); + + canComplete.Set(); + await waitSource.Task; + + var completedTask = + await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken)); + Assert.Same(task, completedTask); + + Assert.True(task is {IsCompleted: true, Status: TaskStatus.RanToCompletion}); + } + + #endregion + + #region RunAsync - Cancellation Tests + + [Fact] + public async Task RunAsync_AlreadyCancelledToken_DoesNotExecuteAnyStep() + { + var runner = CreateStepRunner(); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var executed = false; + var step = new TestStep(_ => + { + executed = true; + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(cts.Token); + + Assert.False(executed); + Assert.Empty(runner.ExecutedSteps); + } + + [Fact] + public async Task RunAsync_CancellationToken_IsCancellableAndLinkedToRunner() + { + var runner = CreateStepRunner(); + var cts = new CancellationTokenSource(); + var tokenWasCancellable = false; + var tokenWasCancelledAfterRequest = false; + + var step = new TestStep(ct => + { + tokenWasCancellable = ct.CanBeCanceled; + cts.Cancel(); + tokenWasCancelledAfterRequest = ct.IsCancellationRequested; + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(cts.Token); + + Assert.True(tokenWasCancellable, "Token passed to step should be cancellable"); + Assert.True(tokenWasCancelledAfterRequest, "Token should reflect cancellation request"); + } + + [Fact] + public async Task RunAsync_StepAddedAfterCancellation_IsNotExecuted() + { + if (!SupportsAddingStepsAfterCancellation) + return; + + var runner = CreateStepRunner(); + var cts = new CancellationTokenSource(); + var step1Executed = false; + var step2Executed = false; + var barrier = new ManualResetEventSlim(false); + var step1Started = new ManualResetEventSlim(false); + + var step1 = new TestStep(async _ => + { + await Task.Yield(); + step1Started.Set(); + step1Executed = true; + barrier.Wait(TestContext.Current.CancellationToken); + }, ServiceProvider); + + runner.AddStep(step1); + + var runTask = runner.RunAsync(cts.Token); + + step1Started.Wait(TestContext.Current.CancellationToken); + + cts.Cancel(); + + var step2 = new TestStep(_ => + { + step2Executed = true; + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step2); + + barrier.Set(); + FinishAdding(runner); + + await runTask; + + Assert.True(step1Executed); + Assert.False(step2Executed, "Step added after cancellation should not execute"); + Assert.Contains(step1, runner.ExecutedSteps); + Assert.DoesNotContain(step2, runner.ExecutedSteps); + } + + [Fact] + public async Task RunAsync_SequentialRunner_CancellationStopsQueuedSteps() + { + if (!SupportsSequentialExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: true); + var cts = new CancellationTokenSource(); + var stepsExecuted = new List(); + + var step1 = new TestStep(_ => + { + stepsExecuted.Add("Step1"); + cts.Cancel(); + return Task.CompletedTask; + }, ServiceProvider); + + var step2 = new TestStep(_ => + { + stepsExecuted.Add("Step2"); + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step1); + runner.AddStep(step2); + FinishAdding(runner); + + await runner.RunAsync(cts.Token); + + Assert.Single(stepsExecuted); + Assert.Equal("Step1", stepsExecuted[0]); + } + + [Fact] + public async Task RunAsync_StepThrowsOperationCanceledException_TreatedAsError() + { + var runner = CreateStepRunner(); + var errorRaised = false; + runner.Error += (_, _) => errorRaised = true; + + var step = new TestStep(_ => throw new OperationCanceledException(), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.True(errorRaised); + Assert.NotNull(runner.Exception); + } + + [Fact] + public async Task RunAsync_StepThrowsTaskCanceledException_TreatedAsError() + { + var runner = CreateStepRunner(); + var errorRaised = false; + runner.Error += (_, _) => errorRaised = true; + + var step = new TestStep(_ => throw new TaskCanceledException(), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.True(errorRaised); + Assert.NotNull(runner.Exception); + } + + #endregion + + #region RunAsync - Error Handling Tests + + [Fact] + public async Task RunAsync_StepThrows_OtherStepsStillExecute() + { + var runner = CreateStepRunner(); + var executedSteps = new ConcurrentBag(); + var failingStepStarted = new ManualResetEventSlim(false); + var failingStepCanThrow = new ManualResetEventSlim(false); + + var failingStep = new TestStep(async _ => + { + await Task.Yield(); + executedSteps.Add("FailingStep"); + failingStepStarted.Set(); + failingStepCanThrow.Wait(TestContext.Current.CancellationToken); + throw new InvalidOperationException("Test error"); + }, ServiceProvider); + + var successStep = new TestStep(_ => + { + executedSteps.Add("SuccessStep"); + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(failingStep); + runner.AddStep(successStep); + FinishAdding(runner); + + var runTask = runner.RunAsync(CancellationToken.None); + + failingStepStarted.Wait(TestContext.Current.CancellationToken); + failingStepCanThrow.Set(); + + await runTask; + + Assert.Contains("FailingStep", executedSteps); + Assert.Contains("SuccessStep", executedSteps); + Assert.Equal(2, runner.ExecutedSteps.Count); + Assert.NotNull(runner.Exception); + } + + [Fact] + public async Task RunAsync_StepThrows_SetsStepErrorProperty() + { + var runner = CreateStepRunner(); + var expectedException = new InvalidOperationException("Test error"); + + var step = new TestStep(_ => throw expectedException, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.Same(expectedException, step.Error); + } + + [Fact] + public async Task RunAsync_MultipleStepsThrow_AllErrorsRecorded() + { + var runner = CreateStepRunner(); + var exception1 = new InvalidOperationException("Error 1"); + var exception2 = new ArgumentException("Error 2"); + + var step1 = new TestStep(_ => throw exception1, ServiceProvider); + var step2 = new TestStep(_ => throw exception2, ServiceProvider); + + runner.AddStep(step1); + runner.AddStep(step2); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.NotNull(runner.Exception); + Assert.Equal(2, runner.Exception.InnerExceptions.Count); + Assert.Contains(exception1, runner.Exception.InnerExceptions); + Assert.Contains(exception2, runner.Exception.InnerExceptions); + } + + [Fact] + public async Task RunAsync_ReentryNotAllowed_ThrowsInvalidOperationException() + { + var runner = CreateStepRunner(); + var mre = new ManualResetEventSlim(false); + var isRunning = new TaskCompletionSource(); + + var executed = false; + var step = new TestStep(_ => + { + isRunning.SetResult(true); + mre.Wait(); + executed = true; + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + + Assert.False(runner.IsRunning); + + var runTask = runner.RunAsync(CancellationToken.None); + + await isRunning.Task; + Assert.True(runner.IsRunning); + + await Assert.ThrowsAsync(async () => await runner.RunAsync(CancellationToken.None)); + mre.Set(); + + await runTask; + + Assert.True(executed); + Assert.Contains(step, runner.ExecutedSteps); + await Assert.Single(runner.ExecutedSteps); + } + + #endregion + + #region StopRunnerException Tests + + [Fact] + public async Task RunAsync_StopRunnerException_StepAddedAfterException_IsNotExecuted() + { + var runner = CreateStepRunner(); + var step1Executed = false; + var step2Executed = false; + var errorOccurred = new ManualResetEventSlim(false); + + runner.Error += (_, args) => + { + args.Cancel = true; + + if (args.Exception is StopRunnerException) + { + var step2 = new TestStep(_ => + { + step2Executed = true; + return Task.CompletedTask; + }, ServiceProvider); + runner.AddStep(step2); + + errorOccurred.Set(); + } + }; + + var step1 = new TestStep(async _ => + { + await Task.Yield(); + step1Executed = true; + throw new StopRunnerException(); + }, ServiceProvider); + + runner.AddStep(step1); + + var runTask = runner.RunAsync(CancellationToken.None); + + errorOccurred.Wait(TestContext.Current.CancellationToken); + + FinishAdding(runner); + + await runTask; + + Assert.True(step1Executed); + Assert.False(step2Executed, "Step added before StopRunnerException should not execute"); + Assert.NotNull(runner.Exception); + Assert.Contains(runner.Exception.InnerExceptions, e => e is StopRunnerException); + } + + [Fact] + public async Task RunAsync_StopRunnerException_SequentialRunner_StopsQueuedSteps() + { + if (!SupportsSequentialExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: true); + var stepsExecuted = new List(); + + var step1 = new TestStep(_ => + { + stepsExecuted.Add("Step1"); + throw new StopRunnerException(); + }, ServiceProvider); + + var step2 = new TestStep(_ => + { + stepsExecuted.Add("Step2"); + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step1); + runner.AddStep(step2); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.Single(stepsExecuted); + Assert.Equal("Step1", stepsExecuted[0]); + } + + [Fact] + public async Task RunAsync_StepThrowsStopRunnerException_SetsCancelToTrue() + { + var runner = CreateStepRunner(); + StepRunnerErrorEventArgs? errorArgs = null; + + runner.Error += (_, args) => + { + errorArgs = args; + }; + + var step = new TestStep(_ => throw new StopRunnerException(), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.NotNull(errorArgs); + Assert.IsType(errorArgs.Exception); + Assert.True(errorArgs.Cancel, "Cancel should be automatically set to true for StopRunnerException"); + } + + [Fact] + public async Task RunAsync_StepThrowsStopRunnerException_ExceptionIsRecorded() + { + var runner = CreateStepRunner(); + var expectedException = new StopRunnerException(); + + var step = new TestStep(_ => throw expectedException, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.NotNull(runner.Exception); + Assert.Contains(expectedException, runner.Exception.InnerExceptions); + } + + #endregion + + #region Exception Property Tests + + [Fact] + public void Exception_BeforeRun_ReturnsNull() + { + var runner = CreateStepRunner(); + runner.AddStep(new TestStep(_ => throw new Exception(), ServiceProvider)); + + Assert.Null(runner.Exception); + } + + [Fact] + public async Task Exception_NoErrors_ReturnsNull() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.Null(runner.Exception); + } + + [Fact] + public async Task Exception_SingleError_ReturnsAggregateExceptionWithSingleInner() + { + var runner = CreateStepRunner(); + var expectedException = new InvalidOperationException("Test error"); + + var step = new TestStep(_ => throw expectedException, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.NotNull(runner.Exception); + Assert.Single(runner.Exception.InnerExceptions); + Assert.Same(expectedException, runner.Exception.InnerExceptions[0]); + } + + [Fact] + public async Task Exception_MultipleErrors_ReturnsAggregateExceptionWithAllErrors() + { + var runner = CreateStepRunner(); + var exception1 = new InvalidOperationException("Error 1"); + var exception2 = new ArgumentException("Error 2"); + var exception3 = new FormatException("Error 3"); + + runner.AddStep(new TestStep(_ => throw exception1, ServiceProvider)); + runner.AddStep(new TestStep(_ => throw exception2, ServiceProvider)); + runner.AddStep(new TestStep(_ => throw exception3, ServiceProvider)); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.NotNull(runner.Exception); + Assert.Equal(3, runner.Exception.InnerExceptions.Count); + Assert.Contains(exception1, runner.Exception.InnerExceptions); + Assert.Contains(exception2, runner.Exception.InnerExceptions); + Assert.Contains(exception3, runner.Exception.InnerExceptions); + } + + #endregion + + #region Error Event Tests + + [Fact] + public async Task Error_StepThrows_EventIsRaisedWithCorrectArgs() + { + var runner = CreateStepRunner(); + StepRunnerErrorEventArgs? errorArgs = null; + object? sender = null; + + runner.Error += (s, args) => + { + sender = s; + errorArgs = args; + }; + + var expectedException = new InvalidOperationException("Test error"); + var step = new TestStep(_ => throw expectedException, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.NotNull(errorArgs); + Assert.Same(runner, sender); + Assert.Same(step, errorArgs.Step); + Assert.Same(expectedException, errorArgs.Exception); + Assert.False(errorArgs.Cancel); + } + + [Fact] + public async Task Error_MultipleStepsThrow_EventIsRaisedForEachError() + { + var runner = CreateStepRunner(); + var errorCount = 0; + var raisedSteps = new ConcurrentBag(); + + runner.Error += (_, args) => + { + Interlocked.Increment(ref errorCount); + raisedSteps.Add(args.Step!); + }; + + var step1 = new TestStep(_ => throw new Exception("Error 1"), ServiceProvider); + var step2 = new TestStep(_ => throw new Exception("Error 2"), ServiceProvider); + var step3 = new TestStep(_ => throw new Exception("Error 3"), ServiceProvider); + + runner.AddStep(step1); + runner.AddStep(step2); + runner.AddStep(step3); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.Equal(3, errorCount); + Assert.Contains(step1, raisedSteps); + Assert.Contains(step2, raisedSteps); + Assert.Contains(step3, raisedSteps); + } + + [Fact] + public async Task Error_SetCancelToTrue_StepAddedAfterError_IsNotExecuted() + { + var runner = CreateStepRunner(); + var step1Executed = false; + var step2Executed = false; + var errorOccurred = new ManualResetEventSlim(false); + + runner.Error += (_, args) => + { + args.Cancel = true; + errorOccurred.Set(); + }; + + var step1 = new TestStep(async _ => + { + await Task.Yield(); + step1Executed = true; + throw new InvalidOperationException("Test error"); + }, ServiceProvider); + + runner.AddStep(step1); + + var runTask = runner.RunAsync(CancellationToken.None); + + errorOccurred.Wait(TestContext.Current.CancellationToken); + + var step2 = new TestStep(_ => + { + step2Executed = true; + return Task.CompletedTask; + }, ServiceProvider); + + if (SupportsAddingStepsAfterCancellation) + runner.AddStep(step2); + else + Assert.Throws(() => runner.AddStep(step2)); + + FinishAdding(runner); + + await runTask; + + if (SupportsAddingStepsAfterCancellation) + { + Assert.True(step1Executed); + Assert.False(step2Executed, "Step added AFTER error with Cancel=true should not execute"); + } + Assert.NotNull(runner.Exception); + } + + [Fact] + public async Task Error_SetCancelToTrue_SequentialRunner_StopsQueuedSteps() + { + if (!SupportsSequentialExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: true); + var stepsExecuted = new List(); + + runner.Error += (_, args) => + { + args.Cancel = true; + }; + + var step1 = new TestStep(_ => + { + stepsExecuted.Add("Step1"); + throw new InvalidOperationException("Test error"); + }, ServiceProvider); + + var step2 = new TestStep(_ => + { + stepsExecuted.Add("Step2"); + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step1); + runner.AddStep(step2); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.Single(stepsExecuted); + Assert.Equal("Step1", stepsExecuted[0]); + Assert.NotNull(runner.Exception); + } + + [Fact] + public async Task Error_SuccessfulStep_EventIsNotRaised() + { + var runner = CreateStepRunner(); + var errorRaised = false; + runner.Error += (_, _) => errorRaised = true; + + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + Assert.False(errorRaised); + } + + [Fact] + public async Task Error_NoSubscribers_DoesNotThrow() + { + var runner = CreateStepRunner(); + + var step = new TestStep(_ => throw new Exception("Test"), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var exception = await Record.ExceptionAsync(() => runner.RunAsync(CancellationToken.None)); + + Assert.Null(exception); + } + + #endregion + + #region Wait() Tests + + [Fact] + public async Task Wait_AfterSuccessfulRun_CompletesWithoutException() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + var exception = Record.Exception(() => runner.Wait()); + + Assert.Null(exception); + } + + [Fact] + public async Task Wait_AfterFailedRun_ThrowsAggregateException() + { + var runner = CreateStepRunner(); + var expectedException = new InvalidOperationException("Test error"); + var step = new TestStep(_ => throw expectedException, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + var exception = Assert.Throws(() => runner.Wait()); + Assert.Contains(expectedException, exception.InnerExceptions); + } + + [Fact] + public async Task Wait_DuringActiveRun_BlocksUntilCompletion() + { + var runner = CreateStepRunner(); + var stepCompleted = false; + var tcs = new TaskCompletionSource(); + + var step = new TestStep(async _ => + { + await tcs.Task; + stepCompleted = true; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + _ = runner.RunAsync(CancellationToken.None); + + var waitTask = Task.Run(() => runner.Wait(), TestContext.Current.CancellationToken); + + // Give Wait time to start blocking + Thread.Sleep(100); + Assert.False(waitTask.IsCompleted); + + // Complete the step + tcs.SetResult(null); + + // Wait should complete now + var completed = await WaitForTaskWithTimeout(waitTask, TimeSpan.FromSeconds(5)); + Assert.True(completed); + Assert.True(stepCompleted); + } + + [Fact] + public async Task Wait_MultipleCallsAfterRun_AllSucceed() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + runner.Wait(); + runner.Wait(); + runner.Wait(); + } + + #endregion + + #region Wait(TimeSpan) Tests + + [Fact] + public async Task WaitWithTimeout_CompletesInTime_DoesNotThrow() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + var exception = Record.Exception(() => runner.Wait(TimeSpan.FromSeconds(5))); + + Assert.Null(exception); + } + + [Fact] + public void WaitWithTimeout_TimeoutExpires_ThrowsTimeoutException() + { + var runner = CreateStepRunner(); + var tcs = new TaskCompletionSource(); + + var step = new TestStep(async _ => + { + await tcs.Task; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + _ = runner.RunAsync(CancellationToken.None); + + Assert.Throws(() => runner.Wait(TimeSpan.FromMilliseconds(100))); + + tcs.SetResult(null); + } + + [Fact] + public async Task WaitWithTimeout_StepFails_ThrowsAggregateException() + { + var runner = CreateStepRunner(); + var expectedException = new InvalidOperationException("Test error"); + var step = new TestStep(_ => throw expectedException, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + var exception = Assert.Throws(() => runner.Wait(TimeSpan.FromSeconds(5))); + Assert.Contains(expectedException, exception.InnerExceptions); + } + + [Fact] + public void WaitWithTimeout_ZeroTimeout_ThrowsTimeoutExceptionIfNotComplete() + { + var runner = CreateStepRunner(); + var tcs = new TaskCompletionSource(); + + var step = new TestStep(async _ => + { + await tcs.Task; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + _ = runner.RunAsync(CancellationToken.None); + + Assert.Throws(() => runner.Wait(TimeSpan.Zero)); + + tcs.SetResult(null); + } + + [Fact] + public void WaitWithTimeout_NegativeTimeout_ThrowsArgumentOutOfRangeException() + { + var runner = CreateStepRunner(); + FinishAdding(runner); + + _ = runner.RunAsync(CancellationToken.None); + + Assert.Throws(() => runner.Wait(TimeSpan.FromSeconds(-1))); + } + + [Fact] + public void WaitWithTimeout_RunnerNeverRuns_ThrowsTimeoutException() + { + var runner = CreateStepRunner(); + FinishAdding(runner); + Assert.Throws(() => runner.Wait(TimeSpan.FromSeconds(1))); + } + + #endregion + + #region GetAwaiter Tests + + [Fact] + public async Task GetAwaiter_BeforeRun_WaitsUntilRunnerStartedAndCompleted() + { + var runner = CreateStepRunner(); + var stepExecuted = false; + + var step = new TestStep(_ => + { + stepExecuted = true; + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var awaitTask = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaitTask.IsCompleted, "Awaiter should block until runner starts and completes"); + + await runner.RunAsync(CancellationToken.None); + + var completed = await WaitForTaskWithTimeout(awaitTask, TimeSpan.FromSeconds(5)); + Assert.True(completed, "Await task should complete after runner finishes"); + Assert.True(stepExecuted); + } + + [Fact] + public async Task GetAwaiter_DuringRun_WaitsForCompletion() + { + var runner = CreateStepRunner(); + var stepCompleted = false; + var tcs = new TaskCompletionSource(); + + var step = new TestStep(async _ => + { + await tcs.Task; + stepCompleted = true; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var runTask = runner.RunAsync(CancellationToken.None); + + var awaitTask = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaitTask.IsCompleted, "Awaiter should block while runner is executing"); + Assert.False(stepCompleted); + + tcs.SetResult(true); + + var completed = await WaitForTaskWithTimeout(awaitTask, TimeSpan.FromSeconds(5)); + Assert.True(completed, "Awaiter should complete when step finishes"); + Assert.True(stepCompleted); + + await runTask; + } + [Fact] - public void AddStep_Null_Throws() + public async Task GetAwaiter_AfterRun_CompletesImmediately() { var runner = CreateStepRunner(); - Assert.Throws(() => runner.AddStep(null!)); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + + var awaiter = runner.GetAwaiter(); + Assert.True(awaiter.IsCompleted); + + var awaitTask = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + var completed = await WaitForTaskWithTimeout(awaitTask, TimeSpan.FromSeconds(1)); + Assert.True(completed, "Awaiter should complete immediately after run"); } [Fact] - public async Task RunAsync_StepsEmpty() + public async Task GetAwaiter_MultipleAwaitersBeforeRun_AllCompleteWhenRunFinishes() { var runner = CreateStepRunner(); + var step = new TestStep(async _ => await Task.Delay(50, TestContext.Current.CancellationToken), ServiceProvider); + + runner.AddStep(step); FinishAdding(runner); + + var awaitTask1 = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + var awaitTask2 = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + var awaitTask3 = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaitTask1.IsCompleted); + Assert.False(awaitTask2.IsCompleted); + Assert.False(awaitTask3.IsCompleted); + await runner.RunAsync(CancellationToken.None); - Assert.Empty(runner.ExecutedSteps); + + var allCompleted = await WaitForTaskWithTimeout( + Task.WhenAll(awaitTask1, awaitTask2, awaitTask3), + TimeSpan.FromSeconds(5)); + Assert.True(allCompleted, "All awaiters should complete when runner finishes"); + } + + [Fact] + public async Task GetAwaiter_WithStepErrors_CompletesWithoutThrowing() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => throw new InvalidOperationException("Test error"), ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var runTask = runner.RunAsync(CancellationToken.None); + + var exception = await Record.ExceptionAsync(async () => await runner); + + Assert.Null(exception); + Assert.NotNull(runner.Exception); + + await runTask; } [Fact] - public async Task RunAsync_CancelledWhenStarted() + public async Task GetAwaiter_CalledMultipleTimesBeforeAndAfterRun_AllSucceed() { var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var awaiterBefore = runner.GetAwaiter(); + Assert.False(awaiterBefore.IsCompleted); + + await runner.RunAsync(CancellationToken.None); + var awaiterAfter = runner.GetAwaiter(); + Assert.True(awaiterAfter.IsCompleted); +#pragma warning disable xUnit1031 + awaiterAfter.GetResult(); +#pragma warning restore xUnit1031 + } + + [Fact] + public async Task GetAwaiter_MultipleAwaiters_AllCompleteWhenCancelled() + { + var runner = CreateStepRunner(); var cts = new CancellationTokenSource(); + var stepStarted = new ManualResetEventSlim(false); + var canCancel = new ManualResetEventSlim(false); + + var step1 = new TestStep(async ct => + { + await Task.Yield(); + stepStarted.Set(); + canCancel.Wait(TestContext.Current.CancellationToken); + ct.ThrowIfCancellationRequested(); + }, ServiceProvider); + + runner.AddStep(step1); + FinishAdding(runner); + + // Start multiple awaiters before cancellation + var awaitTask = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + + var runTask = runner.RunAsync(cts.Token); + + stepStarted.Wait(TestContext.Current.CancellationToken); + + // Cancel cts.Cancel(); + canCancel.Set(); + + // All awaiters should complete without throwing + var exception = await Record.ExceptionAsync(() => awaitTask); - var ran = false; - var step = new TestStep(_ => ran = true, ServiceProvider); + Assert.Null(exception); + + await runTask; + } + + #endregion + + #region ExecutedSteps Tests + + [Fact] + public void ExecutedSteps_BeforeRun_IsEmpty() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); runner.AddStep(step); + Assert.Empty(runner.ExecutedSteps); + } + + [Fact] + public async Task ExecutedSteps_AfterSuccessfulRun_ContainsAllSteps() + { + var runner = CreateStepRunner(); + var step1 = new TestStep(_ => Task.CompletedTask, ServiceProvider); + var step2 = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + runner.AddStep(step1); + runner.AddStep(step2); FinishAdding(runner); - await runner.RunAsync(cts.Token); + await runner.RunAsync(CancellationToken.None); - Assert.False(ran); - Assert.Empty(runner.ExecutedSteps); + Assert.Equal(2, runner.ExecutedSteps.Count); + Assert.Contains(step1, runner.ExecutedSteps); + Assert.Contains(step2, runner.ExecutedSteps); } [Fact] - public async Task RunAsync_WithError() + public async Task ExecutedSteps_AfterFailedStep_ContainsFailedStep() { var runner = CreateStepRunner(); + var step = new TestStep(_ => throw new Exception(), ServiceProvider); - StepRunnerErrorEventArgs? error = null; - runner.Error += (s, e) => - { - Assert.Same(runner, s); - error = e; - }; + runner.AddStep(step); + FinishAdding(runner); + + await runner.RunAsync(CancellationToken.None); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Assert.Single(runner.ExecutedSteps); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Assert.Contains(step, runner.ExecutedSteps); + } + + [Fact] + public async Task ExecutedSteps_CancelledRun_ContainsOnlyExecutedSteps() + { + if (!SupportsSequentialExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: true); + var cts = new CancellationTokenSource(); - var ran1 = false; - var ran2 = false; var step1 = new TestStep(_ => { - ran1 = true; - throw new Exception("Test"); - }, ServiceProvider); - var step2 = new TestStep(_ => - { - ran2 = true; + cts.Cancel(); + return Task.CompletedTask; }, ServiceProvider); + var step2 = new TestStep(_ => Task.CompletedTask, ServiceProvider); runner.AddStep(step1); runner.AddStep(step2); + FinishAdding(runner); + + await runner.RunAsync(cts.Token); + + Assert.Contains(step1, runner.ExecutedSteps); + Assert.DoesNotContain(step2, runner.ExecutedSteps); + } + + [Fact] + public async Task ExecutedSteps_IsReadOnlyCollection() + { + var runner = CreateStepRunner(); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); + runner.AddStep(step); FinishAdding(runner); await runner.RunAsync(CancellationToken.None); - Assert.NotNull(error); - Assert.False(error.Cancel); - Assert.Same(step1, error.Step); + Assert.IsAssignableFrom>(runner.ExecutedSteps); + } + + #endregion + + #region WorkerCount Tests + + [Fact] + public void WorkerCount_IsAtLeastOne() + { + var runner = CreateStepRunner(); + Assert.True(runner.WorkerCount >= 1); + } + + [Fact] + public void WorkerCount_SequentialRunner_IsOne() + { + if (!SupportsSequentialExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: true); + Assert.Equal(1, runner.WorkerCount); + } + + [Fact] + public void WorkerCount_ParallelRunner_IsGreaterThanOne() + { + if (HasSequentialStepExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: false); + Assert.True(runner.WorkerCount > 1); + } + + #endregion + + #region Parallel Execution Tests + + [Fact] + public async Task RunAsync_ParallelRunner_AllowsConcurrentExecution() + { + if (HasSequentialStepExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: false); + var workerCount = runner.WorkerCount; + + var barrier = new CountdownEvent(workerCount); + var allReachedBarrier = false; + var stepsCompleted = 0; + + for (var i = 0; i < workerCount; i++) + { + var step = new TestStep(async _ => + { + await Task.Yield(); + barrier.Signal(); + allReachedBarrier = barrier.Wait(TimeSpan.FromSeconds(5)); + Interlocked.Increment(ref stepsCompleted); + }, ServiceProvider); + runner.AddStep(step); + } + + FinishAdding(runner); + await runner.RunAsync(CancellationToken.None); + + Assert.True(allReachedBarrier, + $"All {workerCount} steps should run concurrently, but barrier was not reached"); + Assert.Equal(workerCount, stepsCompleted); + } + + [Fact] + public async Task RunAsync_ParallelRunner_RespectsWorkerCount() + { + if (HasSequentialStepExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: false); + var workerCount = runner.WorkerCount; + var concurrentCount = 0; + var maxConcurrent = 0; + var lockObj = new object(); + + for (var i = 0; i < workerCount * 3; i++) + { + var step = new TestStep(async _ => + { + lock (lockObj) + { + concurrentCount++; + maxConcurrent = Math.Max(maxConcurrent, concurrentCount); + } + + await Task.Delay(200, TestContext.Current.CancellationToken); + + lock (lockObj) + { + concurrentCount--; + } + }, ServiceProvider); + runner.AddStep(step); + } - Assert.True(ran1); - Assert.True(ran2); - Assert.Equal("Test", step1.Error!.Message); + FinishAdding(runner); + await runner.RunAsync(CancellationToken.None); + + Assert.True(maxConcurrent <= workerCount, + $"Max concurrent ({maxConcurrent}) exceeded worker count ({workerCount})"); } + #endregion + + #region Thread Safety Tests + [Fact] - public async Task RunAsync() + public async Task AddStep_ConcurrentAdds_AllStepsAdded() { var runner = CreateStepRunner(); + var executedCount = 0; + var tasks = new List(); + + for (var i = 0; i < 100; i++) + { + tasks.Add(Task.Run(() => + { + var step = new TestStep(_ => + { + Interlocked.Increment(ref executedCount); + return Task.CompletedTask; + }, ServiceProvider); + runner.AddStep(step); + }, TestContext.Current.CancellationToken)); + } + + await Task.WhenAll(tasks); + FinishAdding(runner); + await runner.RunAsync(CancellationToken.None); + + Assert.Equal(100, executedCount); + } + + [Fact] + public async Task Error_ConcurrentErrors_AllErrorsRecorded() + { + if (HasSequentialStepExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: false); + var errorCount = 0; - var hasError = false; runner.Error += (_, _) => { - hasError = true; + Interlocked.Increment(ref errorCount); }; - var ranList = new List(); - var tsc = new ManualResetEventSlim(false); + for (var i = 0; i < 50; i++) + { + var i1 = i; + runner.AddStep(new TestStep(_ => throw new Exception($"Error {i1}"), ServiceProvider)); + } + + FinishAdding(runner); + await runner.RunAsync(CancellationToken.None); + + Assert.Equal(50, errorCount); + Assert.NotNull(runner.Exception); + Assert.Equal(50, runner.Exception.InnerExceptions.Count); + } + + #endregion + + #region Step Dependency / Awaiting Tests + + [Fact] + public async Task RunAsync_StepAwaitsCompletedStep_CompletesImmediately() + { + var runner = CreateStepRunner(); + var step1Completed = false; + var step2AwaitedSuccessfully = false; + var step1CompletedEvent = new ManualResetEventSlim(false); + var step1 = new TestStep(_ => { - ranList.Add("Step1"); - tsc.Wait(TestContext.Current.CancellationToken); + step1Completed = true; + step1CompletedEvent.Set(); + return Task.CompletedTask; }, ServiceProvider); - var step2 = new TestStep(_ => ranList.Add("Step2"), ServiceProvider); - + + var step2 = new TestStep(async _ => + { + step1CompletedEvent.Wait(TestContext.Current.CancellationToken); + await step1; + step2AwaitedSuccessfully = step1Completed; + }, ServiceProvider); + runner.AddStep(step1); runner.AddStep(step2); + FinishAdding(runner); - var runnerTask = runner.RunAsync(CancellationToken.None); + await runner.RunAsync(CancellationToken.None); - // Step that was added later, also gets executed - var step3 = new TestStep(_ => ranList.Add("Step3"), ServiceProvider); - runner.AddStep(step3); - tsc.Set(); + Assert.True(step1Completed, "Step1 should have completed"); + Assert.True(step2AwaitedSuccessfully, "Step2 should see Step1 as completed"); + } + + [Fact] + public async Task RunAsync_ParallelRunner_StepAwaitsOtherStep_NoDeadlock() + { + if (HasSequentialStepExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: false); + + // Need at least 2 workers + if (runner.WorkerCount < 2) + return; + + var step1Started = new ManualResetEventSlim(false); + var step1CanComplete = new ManualResetEventSlim(false); + var step1Completed = false; + var step2CompletedAfterStep1 = false; + + var step1 = new TestStep(_ => + { + step1Started.Set(); + step1CanComplete.Wait(TestContext.Current.CancellationToken); + step1Completed = true; + return Task.CompletedTask; + }, ServiceProvider); + + var step2 = new TestStep(async _ => + { + step1Started.Wait(TestContext.Current.CancellationToken); + step1CanComplete.Set(); + await step1; + step2CompletedAfterStep1 = step1Completed; + }, ServiceProvider); + runner.AddStep(step1); + runner.AddStep(step2); FinishAdding(runner); - await runnerTask; + var runTask = runner.RunAsync(CancellationToken.None); + var completed = await WaitForTaskWithTimeout(runTask, TimeSpan.FromSeconds(10)); - Assert.False(hasError); + Assert.True(completed, "Runner should complete without deadlock"); + Assert.True(step1Completed, "Step1 should have completed"); + Assert.True(step2CompletedAfterStep1, "Step2 should complete after Step1"); + } - if (PreservesStepExecutionOrder) - Assert.Equal(["Step1", "Step2", "Step3"], ranList); - else - Assert.Equivalent(new HashSet(["Step1", "Step2", "Step3"]), ranList, true); - - Assert.Equivalent(new ReadOnlyCollection([step1, step2, step3]), runner.ExecutedSteps, true); + [Fact] + public async Task RunAsync_ParallelRunner_MultipleStepsAwaitSameStep_AllComplete() + { + if (HasSequentialStepExecutionOrder) + return; + + var runner = CreateStepRunner(sequential: false); + + var awaiterCount = runner.WorkerCount - 1; + if (awaiterCount < 2) + return; + + var step1CanComplete = new ManualResetEventSlim(false); + var awaitersStarted = new CountdownEvent(awaiterCount); + var completedAwaiters = 0; + + var step1 = new TestStep( _ => + { + awaitersStarted.Wait(TestContext.Current.CancellationToken); + step1CanComplete.Wait(TestContext.Current.CancellationToken); + return Task.CompletedTask; + }, ServiceProvider); + + runner.AddStep(step1); + + for (var i = 0; i < awaiterCount; i++) + { + var step = new TestStep(async _ => + { + awaitersStarted.Signal(); + + if (awaitersStarted.CurrentCount == 0) + step1CanComplete.Set(); + + await step1; + Interlocked.Increment(ref completedAwaiters); + }, ServiceProvider); + runner.AddStep(step); + } + + FinishAdding(runner); + + var runTask = runner.RunAsync(CancellationToken.None); + var completed = await WaitForTaskWithTimeout(runTask, TimeSpan.FromSeconds(10)); + + Assert.True(completed, "Runner should complete without deadlock"); + Assert.Equal(awaiterCount, completedAwaiters); } [Fact] - public async Task RunAsync_Cancellation() + public async Task RunAsync_StepAwaitsFailedStep_ReceivesException() { - var runner = CreateStepRunner(true); + var runner = CreateStepRunner(); + var expectedException = new InvalidOperationException("Step1 failed"); + Exception? caughtException = null; - var ranList = new List(); - var cts = new CancellationTokenSource(); - var step1 = new TestStep(_ => + var step1 = new TestStep(_ => throw expectedException, ServiceProvider); + + var step2 = new TestStep(async _ => { - Task.Delay(1000, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); - ranList.Add("Step1"); - cts.Cancel(); + await Task.Delay(50, TestContext.Current.CancellationToken); + try + { + await step1; + } + catch (Exception ex) + { + caughtException = ex; + } }, ServiceProvider); - var step2 = new TestStep(_ => ranList.Add("Step2"), ServiceProvider); runner.AddStep(step1); runner.AddStep(step2); - FinishAdding(runner); - var runnerTask = runner.RunAsync(cts.Token); - - await runnerTask; + await runner.RunAsync(CancellationToken.None); - Assert.Equal(["Step1"], ranList); - Assert.Contains(step1, runner.ExecutedSteps); - Assert.DoesNotContain(step2, runner.ExecutedSteps); + Assert.NotNull(caughtException); + Assert.Same(expectedException, caughtException); } [Fact] - public async Task RunAsync_StopRunner_ShouldStopExecution() + public void RunAsync_SequentialRunner_StepAwaitsLaterStep_WouldDeadlock() { - var runner = CreateStepRunner(true); + if (!SupportsSequentialExecutionOrder) + return; - StepRunnerErrorEventArgs? args = null; - runner.Error += (_, e) => - { - args = e; - }; + var runner = CreateStepRunner(sequential: true); - var ranList = new List(); - var cts = new CancellationTokenSource(); - var step1 = new TestStep(_ => + var step2 = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + var step1 = new TestStep(async _ => { - Task.Delay(1000, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken); - ranList.Add("Step1"); - cts.Cancel(); + var awaitTask = Task.Run(async () => await step2, TestContext.Current.CancellationToken); + var completed = await WaitForTaskWithTimeout(awaitTask, TimeSpan.FromMilliseconds(500)); + Assert.False(completed, + "Awaiting a later step in sequential mode should not complete (would deadlock)"); }, ServiceProvider); - var step2 = new TestStep(_ => ranList.Add("Step2"), ServiceProvider); runner.AddStep(step1); runner.AddStep(step2); - FinishAdding(runner); - var runnerTask = runner.RunAsync(cts.Token); + var runTask = runner.RunAsync(CancellationToken.None); +#pragma warning disable xUnit1031 + var testCompleted = WaitForTaskWithTimeout(runTask, TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); +#pragma warning restore xUnit1031 - await runnerTask; + Assert.True(testCompleted, "Test should complete (step1 detects the would-be deadlock internally)"); + } - if (PreservesStepExecutionOrder) - { - Assert.NotNull(args); - Assert.True(args.Cancel); + #endregion - Assert.Equal(["Step1"], ranList); - Assert.Contains(step1, runner.ExecutedSteps); - Assert.DoesNotContain(step2, runner.ExecutedSteps); - } + // TODO: Remove + private static async Task WaitForTaskWithTimeout(Task task, TimeSpan timeout) + { + var delayTask = Task.Delay(timeout); + var completedTask = await Task.WhenAny(task, delayTask); + return completedTask == task; } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/StepErrorEventArgsTest.cs b/src/CommonUtilities.SimplePipeline/test/StepErrorEventArgsTest.cs index 6df7c6e..9835a74 100644 --- a/src/CommonUtilities.SimplePipeline/test/StepErrorEventArgsTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/StepErrorEventArgsTest.cs @@ -1,4 +1,6 @@ using System; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using AnakinRaW.CommonUtilities.Testing; using Xunit; @@ -10,7 +12,7 @@ public class StepErrorEventArgsTest : TestBaseWithServiceProvider public void Cancel() { var e = new Exception("Tet"); - var step = new TestStep(_ => { }, ServiceProvider); + var step = new TestStep(_ => Task.CompletedTask, ServiceProvider); var args = new StepRunnerErrorEventArgs(e, step); Assert.Same(step, args.Step); diff --git a/src/CommonUtilities.SimplePipeline/test/StepFailureExceptionTests.cs b/src/CommonUtilities.SimplePipeline/test/StepFailureExceptionTests.cs index 2aa3a98..14afbeb 100644 --- a/src/CommonUtilities.SimplePipeline/test/StepFailureExceptionTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/StepFailureExceptionTests.cs @@ -1,5 +1,7 @@ using System; -using System.Threading; +using System.Collections.Generic; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using AnakinRaW.CommonUtilities.Testing; using Xunit; @@ -13,57 +15,68 @@ public void Ctor_WithNullFailedSteps_ThrowsArgumentNullException() Assert.Throws(() => new StepFailureException(null!)); } + [Fact] + public async Task FailedSteps_IsCopyOfFailedSteps() + { + var step1 = await TestStep.CreateFailed(new Exception("TestError 1"), ServiceProvider); + var step2 = await TestStep.CreateFailed(new Exception("TestError 2"), ServiceProvider); + + List steps = [step1]; + + var exception = new StepFailureException(steps); + + // Adding a step after exception is created + steps.Add(step2); + + var actualStep = Assert.Single(exception.FailedSteps); + Assert.Same(step1, actualStep); + } + [Fact] public void Message_WithNoFailedSteps_ReturnsEmptyString() { var ex = new StepFailureException([]); - Assert.Equal(string.Empty, ex.Message); + Assert.Equal("0 Failed Step(s)", ex.Message); } [Fact] - public void Message_WithOneFailedStep_ReturnsErrorMessage() + public async Task Message_WithOneFailedStep_ReturnsErrorMessage() { - var step = new TestStep(_ => throw new Exception("TestError"), ServiceProvider); - - try - { - step.Run(CancellationToken.None); - } - catch - { - // Ignore - } - + var step = await TestStep.CreateFailed(new Exception("TestError"), ServiceProvider); var ex = new StepFailureException([step]); - - Assert.Equal("Step 'TestStep' failed with error: TestError", ex.Message); + Assert.Equal( + "1 Failed Step(s): " + + "Step 'TestStep' failed with error: TestError", + ex.Message); } [Fact] - public void Message_WithMultipleFailedSteps_ReturnsErrorMessage() + public async Task Message_WithMultipleFailedAndNonFailedSteps_ReturnsErrorMessage() { - var step1 = new TestStep(_ => throw new Exception("TestError1"), ServiceProvider); - var step2 = new TestStep(_ => throw new Exception("TestError2"), ServiceProvider); + var step1 = await TestStep.CreateFailed(new Exception("TestError1"), ServiceProvider); + var step2 = await TestStep.CreateFailed(new Exception("TestError2"), ServiceProvider); + var step3 = await TestStep.CreateFailed(null, ServiceProvider); + + var ex = new StepFailureException([step1, step2, step3]); - try - { - step1.Run(CancellationToken.None); - } - catch - { - // Ignore - } - try - { - step2.Run(CancellationToken.None); - } - catch - { - // Ignore - } + Assert.Equal( + "3 Failed Step(s): " + + "Step 'TestStep' failed with error: TestError1;" + + "Step 'TestStep' failed with error: TestError2;" + + "Step 'TestStep' failed with error: n/a", + ex.Message); + } + + [Fact] + public async Task Message_CalledMultipleTimes() + { + var step1 = await TestStep.CreateFailed(new Exception("TestError1"), ServiceProvider); - var ex = new StepFailureException([step1, step2]); + var ex = new StepFailureException([step1]); - Assert.Equal("Step 'TestStep' failed with error: TestError1;Step 'TestStep' failed with error: TestError2", ex.Message); + var message1 = ex.Message; + var message2 = ex.Message; + + Assert.Equal(message1, message2); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs index 6196163..07cba17 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs @@ -1,106 +1,306 @@ -using System; +using AnakinRaW.CommonUtilities.Testing; +using System; using System.Threading; -using AnakinRaW.CommonUtilities.Testing; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Steps; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; +public class TestStepTest : PipelineStepTestBase +{ + protected override bool StepRespectsCancellationToken => true; + protected override bool StepAddsExceptionsToErrorProperty => true; + protected override bool StepAddsStopRunnerExceptionToErrorProperty => false; + + protected override Type GetExpectedExceptionType(Exception thrownException) + { + // TestStep propagates exceptions as-is + return thrownException.GetType(); + } + + protected override PipelineStep CreateStep() + { + return new TestStep(null, ServiceProvider); + } + + protected override PipelineStep CreateStepWithAction(Func action) + { + return new TestStep(action, ServiceProvider); + } + + [Fact] + public void Ctor_NullArgs_Throws() + { + Assert.Throws(() => new TestStep(null, null!)); + } + + [Fact] + public void ToString_IsTypeName() + { + var step = new TestStep(null, ServiceProvider); + Assert.Equal(step.GetType().Name, step.ToString()); + } +} + public class PipelineStepTest : TestBaseWithServiceProvider { [Fact] public void Ctor_NullArgs_Throws() { - Assert.Throws(() => new TestStep(_ => { }, null!)); + Assert.Throws(() => new TestStep(null, null!)); } [Fact] public void Disposed() { - var step = new TestStep(_ => { }, ServiceProvider); + var step = new TestStep(null, ServiceProvider); step.Dispose(); Assert.True(step.IsDisposed); } [Fact] - public void Run() + public async Task RunAsync_TaskAction() { var ran = false; - var step = new TestStep(_ => { ran = true; }, ServiceProvider); + var step = new TestStep(_ => + { + ran = true; + return Task.CompletedTask; + }, ServiceProvider); - step.Run(CancellationToken.None); + await step.RunAsync(CancellationToken.None); Assert.True(ran); } [Fact] - public void Run_ThrowsException() + public async Task RunAsync_AwaitedAction() + { + var ran = false; + var step = new TestStep(async _ => + { + await Task.Yield(); + ran = true; + }, ServiceProvider); + + await step.RunAsync(CancellationToken.None); + + Assert.True(ran); + } + + [Fact] + public async Task RunAsync_ThrowsException() { var expectedError = new Exception(); var step = new TestStep(_ => throw expectedError, ServiceProvider); - Assert.Throws(() => step.Run(CancellationToken.None)); + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); Assert.Same(expectedError, step.Error); } [Fact] - public void Run_WithCancellation_ThrowsOperationCanceledException() + public async Task RunAsync_WithCancellation_ThrowsOperationCanceledException() { var step = new TestStep(ct => { ct.ThrowIfCancellationRequested(); + return Task.CompletedTask; }, ServiceProvider); var cts = new CancellationTokenSource(); cts.Cancel(); - - Assert.Throws(() => step.Run(cts.Token)); + + await Assert.ThrowsAsync(() => step.RunAsync(cts.Token)); Assert.Null(step.Error); } [Fact] - public void Run_StopRunnerException_IsNotAddedToErrors() + public async Task RunAsync_StopRunnerException_IsNotAddedToErrors() { var step = new TestStep(_ => throw new StopRunnerException(), ServiceProvider); - Assert.Throws(() => step.Run(CancellationToken.None)); + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); Assert.Null(step.Error); } [Fact] - public void Run_AggregateException() + public async Task RunAsync_AggregateException() { var expected = new AggregateException(new Exception("Test")); var step = new TestStep(_ => throw expected, ServiceProvider); - Assert.Throws(() => step.Run(CancellationToken.None)); + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); Assert.Same(expected, step.Error); } [Fact] - public void Run_AggregateException_OriginatedFromOperationCancelled() + public async Task RunAsync_AggregateException_OriginatedFromOperationCancelled() { var expected = new Exception("Test"); var step = new TestStep(_ => throw new AggregateException(new OperationCanceledException(null, expected)), ServiceProvider); - Assert.Throws(() => step.Run(CancellationToken.None)); + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); Assert.Same(expected, step.Error); } [Fact] - public void Run_AggregateException_OriginatedFromOperationCancelled_NoInnerException() + public async Task RunAsync_AggregateException_OriginatedFromOperationCancelled_NoInnerException() { var step = new TestStep(_ => throw new AggregateException(new OperationCanceledException()), ServiceProvider); - Assert.Throws(() => step.Run(CancellationToken.None)); + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); Assert.Null(step.Error); } + [Fact] + public async Task GetAwaiter_AfterCompletion_ReturnsImmediately() + { + var executed = false; + var step = new TestStep(_ => + { + executed = true; + return Task.CompletedTask; + }, ServiceProvider); + + await step.RunAsync(CancellationToken.None); + + await step; + Assert.True(executed); + } + + [Fact] + public async Task GetAwaiter_BeforeStart_WaitsForCompletion() + { + var tcs = new TaskCompletionSource(); + var step = new TestStep(async _ => + { + await tcs.Task; + }, ServiceProvider); + + var awaiterTask = Task.Run(async () => await step, TestContext.Current.CancellationToken); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaiterTask.IsCompleted); + + var runTask = step.RunAsync(CancellationToken.None); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaiterTask.IsCompleted); + + tcs.SetResult(true); + await runTask; + + await awaiterTask; + Assert.True(awaiterTask.IsCompleted); + } + + [Fact] + public async Task GetAwaiter_DuringExecution_WaitsForCompletion() + { + var tcs = new TaskCompletionSource(); + var step = new TestStep(async _ => + { + await tcs.Task; + }, ServiceProvider); + + var runTask = step.RunAsync(CancellationToken.None); + + var awaiterTask = Task.Run(async () => await step, TestContext.Current.CancellationToken); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaiterTask.IsCompleted); + + tcs.SetResult(true); + await runTask; + + await awaiterTask; + Assert.True(awaiterTask.IsCompleted); + } + + [Fact] + public async Task GetAwaiter_PropagatesException() + { + var expected = new InvalidOperationException("Test error"); + var step = new TestStep(_ => throw expected, ServiceProvider); + + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); + await Assert.ThrowsAsync(async () => await step); + } + + [Fact] + public async Task GetAwaiter_MultipleAwaiters_AllComplete() + { + var tcs = new TaskCompletionSource(); + var step = new TestStep(async _ => + { + await tcs.Task; + }, ServiceProvider); + + var awaiter1 = Task.Run(async () => await step, TestContext.Current.CancellationToken); + var awaiter2 = Task.Run(async () => await step, TestContext.Current.CancellationToken); + var awaiter3 = Task.Run(async () => await step, TestContext.Current.CancellationToken); + + var runTask = step.RunAsync(CancellationToken.None); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaiter1.IsCompleted); + Assert.False(awaiter2.IsCompleted); + Assert.False(awaiter3.IsCompleted); + + tcs.SetResult(true); + await runTask; + + await Task.WhenAll(awaiter1, awaiter2, awaiter3); + Assert.True(awaiter1.IsCompleted); + Assert.True(awaiter2.IsCompleted); + Assert.True(awaiter3.IsCompleted); + } + + [Fact] + public async Task GetAwaiter_WithCancellation_PropagatesCancellation() + { + var step = new TestStep(ct => + { + ct.ThrowIfCancellationRequested(); + return Task.CompletedTask; + }, ServiceProvider); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(() => step.RunAsync(cts.Token)); + await Assert.ThrowsAsync(async () => await step); + } + + [Fact] + public async Task GetAwaiter_AfterSuccessfulRun_CanBeAwaitedMultipleTimes() + { + var executionCount = 0; + var step = new TestStep(_ => + { + Interlocked.Increment(ref executionCount); + return Task.CompletedTask; + }, ServiceProvider); + + await step.RunAsync(CancellationToken.None); + Assert.Equal(1, executionCount); + + await step; + await step; + await step; + + Assert.Equal(1, executionCount); + } + + [Fact] public void ToString_IsTypeName() { - var step = new TestStep(_ => { }, ServiceProvider); + var step = new TestStep(null, ServiceProvider); Assert.Equal(step.GetType().Name, step.ToString()); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestBase.cs new file mode 100644 index 0000000..758e7ac --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestBase.cs @@ -0,0 +1,400 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Steps; +using AnakinRaW.CommonUtilities.Testing; +using Xunit; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; + +/// +/// Abstract base class for testing PipelineStep implementations. +/// Subclasses MUST define expected behavior via abstract properties. +/// +public abstract class PipelineStepTestBase : TestBaseWithServiceProvider +{ + /// + /// Factory method to create a step for basic testing. + /// + protected abstract PipelineStep CreateStep(); + + /// + /// Factory method to create a step that performs an async action. + /// + protected abstract PipelineStep CreateStepWithAction(Func action); + + /// + /// Defines whether this step respects cancellation tokens. + /// + protected abstract bool StepRespectsCancellationToken { get; } + + /// + /// Defines whether exceptions thrown in RunCoreAsync are added to the Error property. + /// + protected abstract bool StepAddsExceptionsToErrorProperty { get; } + + /// + /// Defines whether StopRunnerException is added to the Error property. + /// + protected abstract bool StepAddsStopRunnerExceptionToErrorProperty { get; } + + /// + /// Defines whether the Step throws exceptions at all + /// + protected virtual bool StepThrowsExceptions => true; + + /// + /// Defines the exception type that is propagated when the step throws an exception. + /// Return null if the step does not throw exceptions + /// + protected abstract Type? GetExpectedExceptionType(Exception thrownException); + + #region Dispose + + [Fact] + public void Disposed() + { + var step = CreateStep(); + + step.Dispose(); + Assert.True(step.IsDisposed); + } + + #endregion + + #region RunAsync + + [Fact] + public async Task RunAsync_TaskAction() + { + var ran = false; + var step = CreateStepWithAction(_ => + { + ran = true; + return Task.CompletedTask; + }); + + await step.RunAsync(CancellationToken.None); + + Assert.True(ran); + } + + [Fact] + public async Task RunAsync_AwaitedAction() + { + var ran = false; + var step = CreateStepWithAction(async _ => + { + await Task.Yield(); + ran = true; + }); + + await step.RunAsync(CancellationToken.None); + + Assert.True(ran); + } + + [Fact] + public async Task RunAsync_ThrowsException() + { + var expectedError = new InvalidOperationException("Test"); + var step = CreateStepWithAction(_ => throw expectedError); + + var expectedType = GetExpectedExceptionType(expectedError); + + if (expectedType != null) + { + await Assert.ThrowsAsync(expectedType, () => step.RunAsync(CancellationToken.None)); + + if (StepAddsExceptionsToErrorProperty) + { + Assert.NotNull(step.Error); + // For transformed exceptions, check if original is preserved somehow + if (expectedType == expectedError.GetType()) + Assert.Same(expectedError, step.Error); + } + else + { + Assert.Null(step.Error); + } + } + else + { + await step.RunAsync(CancellationToken.None); + } + } + + [Fact] + public async Task RunAsync_WithCancellation_ThrowsOperationCanceledException() + { + if (!StepRespectsCancellationToken) + { + // Skip test or verify step ignores cancellation + var tcs = new TaskCompletionSource(); + var step = CreateStepWithAction(async _ => await tcs.Task); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var runTask = step.RunAsync(cts.Token); + await Task.Delay(50, TestContext.Current.CancellationToken); + + Assert.False(runTask.IsCompleted); // Step ignores cancellation + tcs.SetResult(true); + await runTask; + } + else + { + var step2 = CreateStepWithAction(ct => + { + ct.ThrowIfCancellationRequested(); + return Task.CompletedTask; + }); + + var cts2 = new CancellationTokenSource(); + cts2.Cancel(); + + await Assert.ThrowsAsync(() => step2.RunAsync(cts2.Token)); + Assert.Null(step2.Error); + } + } + + [Fact] + public async Task RunAsync_StopRunnerException_IsNotAddedToErrors() + { + if (!StepThrowsExceptions) + return; + + var step = CreateStepWithAction(_ => throw new StopRunnerException()); + + var expectedType = GetExpectedExceptionType(new StopRunnerException())!; + await Assert.ThrowsAsync(expectedType, () => step.RunAsync(CancellationToken.None)); + + if (StepAddsStopRunnerExceptionToErrorProperty) + Assert.NotNull(step.Error); + else + Assert.Null(step.Error); + } + + [Fact] + public async Task RunAsync_AggregateException() + { + if (!StepThrowsExceptions) + return; + + var innerException = new Exception("Test"); + var expected = new AggregateException(innerException); + var step = CreateStepWithAction(_ => throw expected); + + var expectedType = GetExpectedExceptionType(expected)!; + await Assert.ThrowsAsync(expectedType, () => step.RunAsync(CancellationToken.None)); + + if (StepAddsExceptionsToErrorProperty) + { + Assert.NotNull(step.Error); + if (expectedType == typeof(AggregateException)) + Assert.Same(expected, step.Error); + } + else + { + Assert.Null(step.Error); + } + } + + [Fact] + public async Task RunAsync_AggregateException_OriginatedFromOperationCancelled() + { + if (!StepThrowsExceptions) + return; + + var innerException = new Exception("Test"); + var aggregateException = new AggregateException(new OperationCanceledException(null, innerException)); + var step = CreateStepWithAction(_ => throw aggregateException); + + var expectedType = GetExpectedExceptionType(aggregateException)!; + var actualException = await Assert.ThrowsAsync(expectedType, () => step.RunAsync(CancellationToken.None)); + + if (StepAddsExceptionsToErrorProperty) + { + Assert.NotNull(step.Error); + + if (actualException == aggregateException) + { + // Only if we did not modify the exception in the step unwrap to inner exception + Assert.Same(innerException, step.Error); + } + } + else + { + Assert.Null(step.Error); + } + } + + [Fact] + public async Task RunAsync_AggregateException_OriginatedFromOperationCancelled_NoInnerException() + { + if (!StepThrowsExceptions) + return; + + var expected = new AggregateException(new OperationCanceledException()); + var step = CreateStepWithAction(_ => throw expected); + + var expectedType = GetExpectedExceptionType(expected)!; + await Assert.ThrowsAsync(expectedType, async () => await step.RunAsync(CancellationToken.None)); + Assert.Null(step.Error); + } + + #endregion + + #region GetAwaiter + + [Fact] + public async Task GetAwaiter_AfterCompletion_ReturnsImmediately() + { + var executed = false; + var step = CreateStepWithAction(_ => + { + executed = true; + return Task.CompletedTask; + }); + + await step.RunAsync(CancellationToken.None); + + await step; + Assert.True(executed); + } + + [Fact] + public async Task GetAwaiter_BeforeStart_WaitsForCompletion() + { + var tcs = new TaskCompletionSource(); + var step = CreateStepWithAction(async _ => + { + await tcs.Task; + }); + + var awaiterTask = Task.Run(async () => await step, TestContext.Current.CancellationToken); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaiterTask.IsCompleted); + + var runTask = step.RunAsync(CancellationToken.None); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaiterTask.IsCompleted); + + tcs.SetResult(true); + await runTask; + + await awaiterTask; + Assert.True(awaiterTask.IsCompleted); + } + + [Fact] + public async Task GetAwaiter_DuringExecution_WaitsForCompletion() + { + var tcs = new TaskCompletionSource(); + var step = CreateStepWithAction(async _ => + { + await tcs.Task; + }); + + var runTask = step.RunAsync(CancellationToken.None); + + var awaiterTask = Task.Run(async () => await step, TestContext.Current.CancellationToken); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaiterTask.IsCompleted); + + tcs.SetResult(true); + await runTask; + + await awaiterTask; + Assert.True(awaiterTask.IsCompleted); + } + + [Fact] + public async Task GetAwaiter_MultipleAwaiters_AllComplete() + { + var tcs = new TaskCompletionSource(); + var step = CreateStepWithAction(async _ => + { + await tcs.Task; + }); + + var awaiter1 = Task.Run(async () => await step, TestContext.Current.CancellationToken); + var awaiter2 = Task.Run(async () => await step, TestContext.Current.CancellationToken); + var awaiter3 = Task.Run(async () => await step, TestContext.Current.CancellationToken); + + var runTask = step.RunAsync(CancellationToken.None); + + await Task.Delay(50, TestContext.Current.CancellationToken); + Assert.False(awaiter1.IsCompleted); + Assert.False(awaiter2.IsCompleted); + Assert.False(awaiter3.IsCompleted); + + tcs.SetResult(true); + await runTask; + + await Task.WhenAll(awaiter1, awaiter2, awaiter3); + Assert.True(awaiter1.IsCompleted); + Assert.True(awaiter2.IsCompleted); + Assert.True(awaiter3.IsCompleted); + } + + [Fact] + public async Task GetAwaiter_AfterSuccessfulRun_CanBeAwaitedMultipleTimes() + { + var executionCount = 0; + var step = CreateStepWithAction(_ => + { + Interlocked.Increment(ref executionCount); + return Task.CompletedTask; + }); + + await step.RunAsync(CancellationToken.None); + Assert.Equal(1, executionCount); + + await step; + await step; + await step; + + Assert.Equal(1, executionCount); + } + + [Fact] + public async Task GetAwaiter_PropagatesException() + { + if (!StepThrowsExceptions) + return; + + var expected = new InvalidOperationException("Test error"); + var step = CreateStepWithAction(_ => throw expected); + + var expectedType = GetExpectedExceptionType(expected)!; + + await Assert.ThrowsAsync(expectedType, () => step.RunAsync(CancellationToken.None)); + await Assert.ThrowsAsync(expectedType, async () => await step); + } + + [Fact] + public async Task GetAwaiter_WithCancellation_PropagatesCancellation() + { + if (!StepRespectsCancellationToken || !StepThrowsExceptions) + return; + + var step = CreateStepWithAction(ct => + { + ct.ThrowIfCancellationRequested(); + return Task.CompletedTask; + }); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(() => step.RunAsync(cts.Token)); + await Assert.ThrowsAsync(async () => await step); + } + + #endregion +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs index f0edc53..5a66303 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs @@ -1,80 +1,278 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using AnakinRaW.CommonUtilities.SimplePipeline.Runners; using AnakinRaW.CommonUtilities.SimplePipeline.Steps; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class RunPipelineStepTest : TestBaseWithServiceProvider +public class RunPipelineStepTest : PipelineStepTestBase { - private class DelegatePipeline(Func action, IServiceProvider serviceProvider) : Pipeline(serviceProvider) + protected override bool StepRespectsCancellationToken => true; + + protected override bool StepAddsExceptionsToErrorProperty => true; + + protected override bool StepAddsStopRunnerExceptionToErrorProperty => false; + + protected override PipelineStep CreateStep() { - protected override Task PrepareCoreAsync() - { - return Task.FromResult(true); - } + var steps = new List(); + var pipeline = new TestPipeline(ServiceProvider, RunnerBehavior.Sequential, steps); + return new RunPipelineStep(pipeline, ServiceProvider); + } - protected override Task RunCoreAsync(CancellationToken token) + protected override PipelineStep CreateStepWithAction(Func action) + { + var testStep = new TestStep(action, ServiceProvider); + var pipeline = new TestPipeline(ServiceProvider, RunnerBehavior.Sequential, [testStep]); + return new RunPipelineStep(pipeline, ServiceProvider); + } + + protected override Type GetExpectedExceptionType(Exception thrownException) + { + if (thrownException is AggregateException aggregateException && aggregateException.IsExceptionType()) { - return action(token); + return aggregateException.InnerExceptions.FirstOrDefault(p => p.IsExceptionType()) + ?.InnerException is not null ? typeof(StepFailureException) : typeof(OperationCanceledException); } + + if (thrownException.IsExceptionType() || thrownException is StopRunnerException) + return typeof(OperationCanceledException); + return typeof(StepFailureException); } + #region Constructor + [Fact] - public void Run() + public void Constructor_NullPipeline_ThrowsArgumentNullException() { - var ran = false; + Assert.Throws(() => new RunPipelineStep(null!, ServiceProvider)); + } - var pipeline = new DelegatePipeline(async ct => - { - await Task.Delay(3000, ct).ConfigureAwait(false); - ran = true; + [Fact] + public void Constructor_NullServiceProvider_ThrowsArgumentNullException() + { + var pipeline = new TestPipeline(ServiceProvider, RunnerBehavior.Sequential, []); + Assert.Throws(() => new RunPipelineStep(pipeline, null!)); + } - },ServiceProvider); + [Fact] + public void Constructor_ValidArguments_CreatesInstance() + { + var pipeline = new TestPipeline(ServiceProvider, RunnerBehavior.Sequential, []); var step = new RunPipelineStep(pipeline, ServiceProvider); - step.Run(CancellationToken.None); - Assert.True(ran); + Assert.NotNull(step); + Assert.False(step.IsDisposed); } + #endregion + + #region RunAsync + [Fact] - public void Run_PipelineFails() + public async Task RunAsync_ExecutesPipelineSteps() { - var pipeline = new DelegatePipeline(_ => throw new ApplicationException("test"), ServiceProvider); + var step1Executed = false; + var step2Executed = false; - var step = new RunPipelineStep(pipeline, ServiceProvider); + var testStep1 = new TestStep(_ => + { + step1Executed = true; + return Task.CompletedTask; + }, ServiceProvider); + + var testStep2 = new TestStep(_ => + { + step2Executed = true; + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = new TestPipeline(ServiceProvider, RunnerBehavior.Sequential, [testStep1, testStep2]); + var runPipelineStep = new RunPipelineStep(pipeline, ServiceProvider); + + await runPipelineStep.RunAsync(CancellationToken.None); - Assert.Throws(() => step.Run(CancellationToken.None)); + Assert.True(step1Executed); + Assert.True(step2Executed); } [Fact] - public void Run_Cancel() + public async Task RunAsync_ExecutesStepsInOrder() { - var cts = new CancellationTokenSource(); + var executionOrder = new List(); + + var testStep1 = new TestStep(_ => + { + executionOrder.Add(1); + return Task.CompletedTask; + }, ServiceProvider); + + var testStep2 = new TestStep(_ => + { + executionOrder.Add(2); + return Task.CompletedTask; + }, ServiceProvider); - var pipeline = new DelegatePipeline(async ct => + var testStep3 = new TestStep(_ => { - await Task.Run(() => cts.Cancel(), CancellationToken.None); - ct.ThrowIfCancellationRequested(); + executionOrder.Add(3); + return Task.CompletedTask; }, ServiceProvider); + var pipeline = new TestPipeline(ServiceProvider, RunnerBehavior.Sequential, [testStep1, testStep2, testStep3]); + var runPipelineStep = new RunPipelineStep(pipeline, ServiceProvider); + + await runPipelineStep.RunAsync(CancellationToken.None); + + Assert.Equal([1,2,3], executionOrder); + } + + [Fact] + public async Task RunAsync_CancellationToken_IsPropagatedToSteps() + { + var tcs = new TaskCompletionSource(); + var stepRunning = new TaskCompletionSource(); + var cancellationObserved = false; + + var testStep = new TestStep(async ct => + { + try + { + stepRunning.SetResult(true); + await tcs.Task.WaitAsync(ct); + } + catch (OperationCanceledException) + { + cancellationObserved = true; + throw; + } + }, ServiceProvider); + + var steps = new List { testStep }; + var pipeline = new TestPipeline(ServiceProvider, RunnerBehavior.Sequential, steps); + var runPipelineStep = new RunPipelineStep(pipeline, ServiceProvider); + + using var cts = new CancellationTokenSource(); + var runTask = runPipelineStep.RunAsync(cts.Token); + + await stepRunning.Task; + cts.Cancel(); + + await Assert.ThrowsAsync(() => runTask); + Assert.True(cancellationObserved); + } + + [Fact] + public async Task RunAsync_EmptyPipeline_CompletesSuccessfully() + { + var steps = new List(); + var pipeline = new TestPipeline(ServiceProvider, RunnerBehavior.Sequential, steps); + var runPipelineStep = new RunPipelineStep(pipeline, ServiceProvider); + + await runPipelineStep.RunAsync(CancellationToken.None); + + Assert.Null(runPipelineStep.Error); + } + + [Fact] + public async Task RunAsync_OnPrepareThrows_PropagatesException() + { + var expectedException = new InvalidOperationException("Prepare failed"); + var steps = new List(); + var pipeline = new TestPipeline( + ServiceProvider, + RunnerBehavior.Sequential, + steps, + _ => throw expectedException); + var runPipelineStep = new RunPipelineStep(pipeline, ServiceProvider); + + var exception = await Assert.ThrowsAsync( + () => runPipelineStep.RunAsync(CancellationToken.None)); + + Assert.Same(expectedException, exception); + } + + [Fact] + public async Task RunAsync_PipelineThrowsStepFailedException() + { + var expectedException = new InvalidOperationException("Pipeline step failed"); + var step = new TestStep(_ => throw expectedException, ServiceProvider); + var pipeline = new TestPipeline( + ServiceProvider, + RunnerBehavior.Sequential, + [step]); + + var runPipelineStep = new RunPipelineStep(pipeline, ServiceProvider); + + var exception = await Assert.ThrowsAsync( + () => runPipelineStep.RunAsync(CancellationToken.None)); + + Assert.Contains("Pipeline step failed", exception.Message); + } + + #endregion + + #region Dispose + + [Fact] + public void Dispose_DisposesPipeline() + { + var pipeline = new TestPipeline( + ServiceProvider, + RunnerBehavior.Sequential, + new List()); var step = new RunPipelineStep(pipeline, ServiceProvider); - Assert.ThrowsAny(() => step.Run(cts.Token)); + step.Dispose(); + step.Dispose(); + step.Dispose(); + + Assert.True(pipeline.IsDisposed); + Assert.True(step.IsDisposed); } [Fact] - public void Dispose() + public async Task Dispose_AfterRun_DisposesPipeline() { - var pipeline = new DelegatePipeline(_ => Task.CompletedTask, ServiceProvider); + var pipeline = new TestPipeline( + ServiceProvider, + RunnerBehavior.Sequential, + new List()); var step = new RunPipelineStep(pipeline, ServiceProvider); - + + await step.RunAsync(CancellationToken.None); step.Dispose(); Assert.True(step.IsDisposed); - Assert.True(pipeline.IsDisposed); + } + + #endregion + + private class TestPipeline( + IServiceProvider serviceProvider, + RunnerBehavior runnerBehavior, + IList steps, + Func? onPrepare = null) : StepRunnerPipeline(serviceProvider) + { + protected override IStepRunner CreateRunner() + { + if (runnerBehavior is RunnerBehavior.Sequential) + return new SequentialStepRunner(ServiceProvider); + return new AsyncStepRunner(4, ServiceProvider); + } + + protected override async Task> CreateRunnerSteps(CancellationToken token) + { + if (onPrepare is not null) + await onPrepare(token); + return steps; + } } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/SynchronizedStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/SynchronizedStepTest.cs deleted file mode 100644 index 51142dc..0000000 --- a/src/CommonUtilities.SimplePipeline/test/Steps/SynchronizedStepTest.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.Testing; -using Xunit; - -namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; - -public class SynchronizedStepTest : TestBaseWithServiceProvider -{ - [Fact] - public void Wait_ThrowsTimeoutException() - { - var step = new TestSyncStep(_ => { }, ServiceProvider); - // Do not run the step - Assert.Throws(() => step.Wait(TimeSpan.Zero)); - } - - [Fact] - public void Run_ThrowsWait() - { - var expectedException = new Exception("Test"); - var step = new TestSyncStep(_ => throw expectedException, ServiceProvider); - - Assert.Throws(() => step.Run(CancellationToken.None)); - - // Should not block - step.Wait(); - } - - [Fact] - public void Run_Cancelled_ThrowsOperationCanceledException() - { - var step = new TestSyncStep(ct => { ct.ThrowIfCancellationRequested(); }, ServiceProvider); - - var flag = false; - step.Canceled += delegate - { - flag = true; - }; - - var cts = new CancellationTokenSource(); - cts.Cancel(); - - Assert.Throws(() => step.Run(cts.Token)); - - // Should not block - step.Wait(); - - Assert.True(flag); - } - - [Fact] - public void Wait() - { - var flag = false; - var step = new TestSyncStep(_ => - { - Task.Delay(1000, CancellationToken.None).Wait(CancellationToken.None); - flag = true; - }, ServiceProvider); - - Task.Run(() => step.Run(CancellationToken.None), TestContext.Current.CancellationToken).Forget(); - - step.Wait(); - - Assert.True(flag); - } - - [Fact] - public void Wait_WithTimeout() - { - var step = new TestSyncStep(_ => - { - Task.Delay(1000, CancellationToken.None).Wait(CancellationToken.None); - }, ServiceProvider); - - Task.Factory.StartNew(() => step.Run(CancellationToken.None), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - Assert.Throws(() => step.Wait(TimeSpan.FromMilliseconds(100))); - } - - [Fact] - public void Dispose() - { - var step = new TestSyncStep(_ => - { - Task.Delay(1000, CancellationToken.None).Wait(CancellationToken.None); - }, ServiceProvider); - - step.Dispose(); - - Assert.Throws(step.Wait); - } -} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs index 972f1d3..a39e73a 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs @@ -1,42 +1,144 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using AnakinRaW.CommonUtilities.SimplePipeline.Runners; using AnakinRaW.CommonUtilities.SimplePipeline.Steps; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class WaitStepTest : TestBaseWithServiceProvider -{ +public class WaitStepTest : PipelineStepTestBase +{ + protected override bool StepRespectsCancellationToken => false; // WaitStep ignores cancellation! + protected override bool StepAddsExceptionsToErrorProperty => false; // Transforms to StopRunnerException + protected override bool StepAddsStopRunnerExceptionToErrorProperty => false; + + protected override Type GetExpectedExceptionType(Exception thrownException) + { + // WaitStep transforms runner exceptions into StopRunnerException + return typeof(StopRunnerException); + } + + protected override PipelineStep CreateStep() + { + var runner = new AsyncStepRunner(1, ServiceProvider); + return new WaitStep(runner, ServiceProvider); + } + + protected override PipelineStep CreateStepWithAction(Func action) + { + var runner = new AsyncStepRunner(1, ServiceProvider); + runner.AddStep(new TestStep(action, ServiceProvider)); + _ = runner.RunAsync(CancellationToken.None); + return new WaitStep(runner, ServiceProvider); + } + + [Fact] + public void Ctor_NullArgs_Throws() + { + Assert.Throws(() => new WaitStep(null!, ServiceProvider)); + Assert.Throws(() => new WaitStep( + new AsyncStepRunner(1, ServiceProvider), + null!)); + } + [Fact] - public void Wait() + public void ToString_HasExpectedValue() { - var runner = new ParallelStepRunner(2, ServiceProvider); + var runner = new AsyncStepRunner(1, ServiceProvider); + var step = new WaitStep(runner, ServiceProvider); + Assert.Equal("Waiting for other steps", step.ToString()); + } + + [Fact] + public async Task RunAsync_EmptyRunner_CompletesSuccessfully() + { + var runner = new AsyncStepRunner(1, ServiceProvider); + // No steps added + + var step = new WaitStep(runner, ServiceProvider); + + await runner.RunAsync(CancellationToken.None); + await step.RunAsync(CancellationToken.None); + + Assert.Null(step.Error); + } + + [Fact] + public async Task RunAsync_CompletesAfterRunnerFinishes() + { + var runner = new AsyncStepRunner(2, ServiceProvider); var completed1 = false; var completed2 = false; runner.AddStep(new TestStep(_ => { - Task.Delay(1000, CancellationToken.None).Wait(CancellationToken.None); completed1 = true; + return Task.CompletedTask; }, ServiceProvider)); runner.AddStep(new TestStep(_ => { - Task.Delay(1000, CancellationToken.None).Wait(CancellationToken.None); completed2 = true; + return Task.CompletedTask; }, ServiceProvider)); var step = new WaitStep(runner, ServiceProvider); _ = runner.RunAsync(CancellationToken.None); - step.Run(CancellationToken.None); - // We cannot assert on the runnerTask task, - // as the impl. creates different tasks for await and Wait(). - // This may result in a race where the Wait() reports completion before the awaitable task + await step.RunAsync(CancellationToken.None); + Assert.False(runner.IsRunning); Assert.True(completed1); Assert.True(completed2); } + + [Fact] + public async Task Wait_RunnerWithException_ThrowsStopRunnerException() + { + var runner = new AsyncStepRunner(1, ServiceProvider); + runner.AddStep(new TestStep(_ => throw new InvalidOperationException("Test"), ServiceProvider)); + + var step = new WaitStep(runner, ServiceProvider); + + _ = runner.RunAsync(CancellationToken.None); + + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); + Assert.Null(step.Error); + } + + [Fact] + public async Task Wait_RunnerCancelled_ThrowsStopRunnerException() + { + var runner = new AsyncStepRunner(1, ServiceProvider); + var cts = new CancellationTokenSource(); + + runner.AddStep(new TestStep(ct => + { + ct.ThrowIfCancellationRequested(); + return Task.CompletedTask; + }, ServiceProvider)); + + var step = new WaitStep(runner, ServiceProvider); + + cts.Cancel(); + _ = runner.RunAsync(cts.Token); + + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); + Assert.Null(step.Error); + } + + [Fact] + public async Task Wait_RunnerWithStopRunnerException_PropagatesStopRunnerException() + { + var runner = new AsyncStepRunner(1, ServiceProvider); + runner.AddStep(new TestStep(_ => throw new StopRunnerException(), ServiceProvider)); + + var step = new WaitStep(runner, ServiceProvider); + _ = runner.RunAsync(CancellationToken.None); + + await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); + Assert.Null(step.Error); + } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/TestStep.cs b/src/CommonUtilities.SimplePipeline/test/TestData/TestStep.cs similarity index 71% rename from src/CommonUtilities.SimplePipeline/test/TestStep.cs rename to src/CommonUtilities.SimplePipeline/test/TestData/TestStep.cs index dcb44d0..61c2b8c 100644 --- a/src/CommonUtilities.SimplePipeline/test/TestStep.cs +++ b/src/CommonUtilities.SimplePipeline/test/TestData/TestStep.cs @@ -2,25 +2,40 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Steps; using System; using System.Threading; +using System.Threading.Tasks; -namespace AnakinRaW.CommonUtilities.SimplePipeline.Test; +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; public class TestStep : PipelineStep { - private readonly Action? _action; + private readonly Func? _action; protected TestStep(IServiceProvider sp) : base(sp) { } - public TestStep(Action action, IServiceProvider serviceProvider) : base(serviceProvider) + public TestStep(Func? action, IServiceProvider serviceProvider) : base(serviceProvider) { _action = action; } - protected override void RunCore(CancellationToken token) + protected override Task RunCoreAsync(CancellationToken token) { - _action?.Invoke(token); + return _action?.Invoke(token) ?? Task.CompletedTask; + } + + public static async Task CreateFailed(Exception? error, IServiceProvider serviceProvider) + { + var step = new TestStep(_ => error is not null ? throw error : Task.CompletedTask, serviceProvider); + try + { + await step.RunAsync(CancellationToken.None); + } + catch (Exception) + { + // Ignore + } + return step; } } @@ -83,15 +98,4 @@ public override int GetHashCode() { return HashCode.Combine(Progress, Aggregated); } -} - -public class TestSyncStep(Action? action, IServiceProvider serviceProvider) - : SynchronizedStep(serviceProvider) -{ - public ProgressType Type => new() { Id = "test", DisplayName = "Test" }; - - protected override void RunSynchronized(CancellationToken token) - { - action?.Invoke(token); - } } \ No newline at end of file diff --git a/src/CommonUtilities.Testing/Collections/CollectionsTestSuite.cs b/src/CommonUtilities.Testing/Collections/CollectionsTestSuite.cs index 5e7cd47..c226d78 100644 --- a/src/CommonUtilities.Testing/Collections/CollectionsTestSuite.cs +++ b/src/CommonUtilities.Testing/Collections/CollectionsTestSuite.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using Xunit; namespace AnakinRaW.CommonUtilities.Testing.Collections; @@ -48,10 +47,8 @@ public enum ModifyOperation /// /// Provides a collection of valid sizes for testing collections. /// - /// - /// An of object arrays, where each array contains - /// a single integer representing a valid collection size. - /// + /// This data is intended for use with xUnit's Theory tests to verify behavior across different + /// collection sizes, including empty, single-item, and larger collections. public static IEnumerable ValidCollectionSizes() { yield return [0]; @@ -75,9 +72,9 @@ public static IEnumerable ValidCollectionSizes() /// public static IEnumerable GetEnumerableTestData() { - foreach (var collectionSizeArray in ValidCollectionSizes()) + foreach (var sizeArray in ValidCollectionSizes()) { - var count = (int)collectionSizeArray[0]; + var count = (int)sizeArray[0]; yield return [count, 0, 0, 0]; // Empty Enumerable yield return [count, count + 1, 0, 0]; // Enumerable that is 1 larger @@ -157,7 +154,7 @@ protected IEnumerable CreateList(IEnumerable? enumerableToMatchTo, int cou // Add Matching elements if (enumerableToMatchTo != null) { - match = enumerableToMatchTo.ToList(); + match = [.. enumerableToMatchTo]; for (var i = 0; i < numberOfMatchingElements; i++) { list.Add(match[i]); diff --git a/src/CommonUtilities.Testing/Collections/IReadOnlyCollectionTestSuite.cs b/src/CommonUtilities.Testing/Collections/IReadOnlyCollectionTestSuite.cs deleted file mode 100644 index 18607fd..0000000 --- a/src/CommonUtilities.Testing/Collections/IReadOnlyCollectionTestSuite.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Xunit; - -namespace AnakinRaW.CommonUtilities.Testing.Collections; - -// This test suite is taken from the .NET runtime repository (https://github.com/dotnet/runtime) and adapted to the VSTesting Framework. -// The .NET Foundation licenses this under the MIT license. -/// -/// Contains tests that ensure the correctness of any class that implements the generic -/// interface -/// -[SuppressMessage("ReSharper", "InconsistentNaming")] -public abstract class IReadOnlyCollectionTestSuite : IEnumerableTestSuite -{ - /// - /// Creates an instance of an that can be used for testing. - /// - /// An instance of an that can be used for testing. - protected abstract IReadOnlyCollection GenericIReadOnlyCollectionFactory(IEnumerable baseCollection); - - /// - protected override IEnumerable GenericIEnumerableFactory(int count) - { - return GenericIReadOnlyCollectionFactory(count); - } - - /// - /// Creates an instance of an that can be used for testing. - /// - /// The number of unique items that the returned contains. - /// An instance of an that can be used for testing. - protected virtual IReadOnlyCollection GenericIReadOnlyCollectionFactory(int count) - { - var collection = CreateEnumerable(null, count, 0, 0); - return GenericIReadOnlyCollectionFactory(collection); - } - - /// - protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) - { - yield break; - } - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - - #region Count - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void ICollection_Generic_Count_Validity(int count) - { - var collection = GenericIReadOnlyCollectionFactory(count); - Assert.Equal(count, collection.Count); - } - - #endregion - -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member -} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/Collections/IReadOnlyListTestSuite.cs b/src/CommonUtilities.Testing/Collections/IReadOnlyListTestSuite.cs deleted file mode 100644 index 61fc432..0000000 --- a/src/CommonUtilities.Testing/Collections/IReadOnlyListTestSuite.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; -using Xunit; - -namespace AnakinRaW.CommonUtilities.Testing.Collections; - -// This test suite is taken from the .NET runtime repository (https://github.com/dotnet/runtime) and adapted to the VSTesting Framework. -// The .NET Foundation licenses this under the MIT license. -/// -/// Contains tests that ensure the correctness of any class that implements the generic -/// interface -/// -[SuppressMessage("ReSharper", "InconsistentNaming")] -public abstract class IReadOnlyListTestSuite : IReadOnlyCollectionTestSuite -{ - /// - /// Gets the of the exception that is expected to be thrown - /// when accessing or modifying an with an invalid index. - /// - /// - /// By default, this property returns . - /// Derived classes can override this property to specify a different exception type - /// if the behavior of the implementation differs. - /// - protected virtual Type IList_Generic_Item_InvalidIndex_ThrowType => typeof(ArgumentOutOfRangeException); - - /// - /// Creates an instance of an that can be used for testing. - /// - /// An instance of an that can be used for testing. - protected abstract IReadOnlyList GenericIReadOnlyListFactory(IEnumerable baseCollection); - - /// - /// Creates an instance of an that can be used for testing. - /// - /// The number of unique items that the returned contains. - /// An instance of an that can be used for testing. - protected virtual IReadOnlyList GenericIReadOnlyListFactory(int count) - { - var baseCollection = CreateEnumerable(null, count, 0, 0); - return GenericIReadOnlyListFactory(baseCollection); - } - - /// - protected override IReadOnlyCollection GenericIReadOnlyCollectionFactory(IEnumerable baseCollection) - { - return GenericIReadOnlyListFactory(baseCollection); - } - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - - #region FromEnumerable - - [Theory] - [MemberData(nameof(GetEnumerableTestData))] - #pragma warning disable xUnit1026 - public void From_IEnumerable(int _, int enumerableLength, int __, int numberOfDuplicateElements) - #pragma warning restore xUnit1026 - { - var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements).ToList(); - var list = GenericIReadOnlyListFactory(enumerable); - - var expected = enumerable.ToList(); - - Assert.Equal(enumerableLength, list.Count); - - for (var i = 0; i < enumerableLength; i++) - Assert.Equal(expected[i], list[i]); - } - - #endregion - - #region Item Getter - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IList_Generic_ItemGet_NegativeIndex_ThrowsException(int count) - { - var list = GenericIReadOnlyListFactory(count); - Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[-1]); - Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[int.MinValue]); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IList_Generic_ItemGet_IndexGreaterThanListCount_ThrowsException(int count) - { - var list = GenericIReadOnlyListFactory(count); - Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[count]); - Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[count + 1]); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IList_Generic_ItemGet_ValidGetWithinListBounds(int count) - { - var list = GenericIReadOnlyListFactory(count); - - foreach (var i in Enumerable.Range(0, count)) - Sink(list[i]); - return; - - [MethodImpl(MethodImplOptions.NoInlining)] - static void Sink(T _) { } - } - - #endregion - -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member -} \ No newline at end of file diff --git a/src/CommonUtilities.Testing/CommonUtilities.Testing.csproj b/src/CommonUtilities.Testing/CommonUtilities.Testing.csproj index 46b695b..5896fc6 100644 --- a/src/CommonUtilities.Testing/CommonUtilities.Testing.csproj +++ b/src/CommonUtilities.Testing/CommonUtilities.Testing.csproj @@ -27,13 +27,13 @@ - - - - - + + + + + - + diff --git a/src/CommonUtilities.Testing/EqualityComparers/ConstantHashCodeEqualityComparer.cs b/src/CommonUtilities.Testing/EqualityComparers/ConstantHashCodeEqualityComparer.cs index 1d50573..5377662 100644 --- a/src/CommonUtilities.Testing/EqualityComparers/ConstantHashCodeEqualityComparer.cs +++ b/src/CommonUtilities.Testing/EqualityComparers/ConstantHashCodeEqualityComparer.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace AnakinRaW.CommonUtilities.Testing; +namespace AnakinRaW.CommonUtilities.Testing.EqualityComparers; /// /// Provides an equality comparer for objects of type that always returns a constant hash code. diff --git a/src/CommonUtilities.Testing/EqualityComparers/ReferenceEqualityComparer.cs b/src/CommonUtilities.Testing/EqualityComparers/ReferenceEqualityComparer.cs index c1a9e42..5d04f65 100644 --- a/src/CommonUtilities.Testing/EqualityComparers/ReferenceEqualityComparer.cs +++ b/src/CommonUtilities.Testing/EqualityComparers/ReferenceEqualityComparer.cs @@ -1,9 +1,9 @@ -#if !NET5_0_OR_GREATER +#if !NET10_0_OR_GREATER using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; -namespace AnakinRaW.CommonUtilities.Testing; +namespace AnakinRaW.CommonUtilities.Testing.EqualityComparers; /// /// An that uses reference equality () diff --git a/src/CommonUtilities.Testing/Extensions/RandomExtensions.cs b/src/CommonUtilities.Testing/Extensions/RandomExtensions.cs index e45abac..a94621d 100644 --- a/src/CommonUtilities.Testing/Extensions/RandomExtensions.cs +++ b/src/CommonUtilities.Testing/Extensions/RandomExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace AnakinRaW.CommonUtilities.Extensions; +namespace AnakinRaW.CommonUtilities.Testing.Extensions; /// /// Provides extension methods for the class. @@ -132,9 +132,7 @@ public static ulong ULong() /// /// A randomly selected value from the enumeration type . /// - /// - /// Thrown if is not an enumeration type. - /// + /// is not an enumeration type. public static T Enum() where T : struct, Enum { var values = @@ -154,7 +152,7 @@ public static T Enum() where T : struct, Enum /// The type of the elements in the sequence. /// The sequence of items to select from. /// A randomly selected item from the sequence. - /// Thrown if the sequence is empty. + /// The sequence is empty. public static T Item(IEnumerable items) { T current = default!; diff --git a/src/CommonUtilities.Testing/Extensions/StringExtensions.cs b/src/CommonUtilities.Testing/Extensions/StringExtensions.cs index dd1319c..fc486d3 100644 --- a/src/CommonUtilities.Testing/Extensions/StringExtensions.cs +++ b/src/CommonUtilities.Testing/Extensions/StringExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace AnakinRaW.CommonUtilities.Extensions; +namespace AnakinRaW.CommonUtilities.Testing.Extensions; /// /// Provides extension methods for string manipulation and testing. @@ -16,7 +16,7 @@ public static class StringExtensions /// /// The input string whose character casing will be shuffled. /// A new string with randomly shuffled character casing. - /// Thrown if is null. + /// is . public static unsafe string ShuffleCasing(string input) { if (input is null) diff --git a/src/CommonUtilities.Testing/TestingHelpers.cs b/src/CommonUtilities.Testing/TestingHelpers.cs index 2ccf751..46c0507 100644 --- a/src/CommonUtilities.Testing/TestingHelpers.cs +++ b/src/CommonUtilities.Testing/TestingHelpers.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace AET.Testing; +namespace AnakinRaW.CommonUtilities.Testing; /// /// Provides common helper methods useful creating test code. diff --git a/src/CommonUtilities/src/AwaitExtensions.cs b/src/CommonUtilities/src/AwaitExtensions.cs index 8d4379f..fa8472c 100644 --- a/src/CommonUtilities/src/AwaitExtensions.cs +++ b/src/CommonUtilities/src/AwaitExtensions.cs @@ -48,7 +48,7 @@ Task WaitForExitAsync(this Process process, CancellationToken cancellationToken throw; } - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); try { process.Exited += Handler; diff --git a/src/CommonUtilities/src/Collections/DebugViews.cs b/src/CommonUtilities/src/Collections/DebugViews.cs index 3589da2..0d05abe 100644 --- a/src/CommonUtilities/src/Collections/DebugViews.cs +++ b/src/CommonUtilities/src/Collections/DebugViews.cs @@ -11,17 +11,19 @@ internal sealed class IValueListDictionaryDebugView(IReadOnlyValue private readonly IReadOnlyValueListDictionary _dict = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public DebugViewValueListDictionaryItem[] Items => _dict.Select(keyValuePair => new DebugViewValueListDictionaryItem(keyValuePair)).ToArray(); + public DebugViewValueListDictionaryItem[] Items => + _dict.Select(keyValuePair => new DebugViewValueListDictionaryItem(keyValuePair)) + .ToArray(); } [DebuggerDisplay("{ValueList}", Name = "[{Key}]")] -internal readonly struct DebugViewValueListDictionaryItem(KeyValuePair> keyValue) +internal readonly struct DebugViewValueListDictionaryItem(KeyValuePair> keyValue) { [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] public TKey Key { get; } = keyValue.Key; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public ReadOnlyFrugalList ValueList { get; } = keyValue.Value; + public IReadOnlyList ValueList { get; } = keyValue.Value; } diff --git a/src/CommonUtilities/src/Collections/EmptyEnumerator.cs b/src/CommonUtilities/src/Collections/EmptyEnumerator.cs new file mode 100644 index 0000000..0d1e999 --- /dev/null +++ b/src/CommonUtilities/src/Collections/EmptyEnumerator.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace AnakinRaW.CommonUtilities.Collections; + +internal sealed class EmptyEnumerator : IEnumerator +{ + public static readonly EmptyEnumerator Instance = new(); + + public T Current => throw new InvalidOperationException("Enumeration has not started."); + + object? IEnumerator.Current => Current; + + private EmptyEnumerator() { } + + public bool MoveNext() + { + return false; + } + + public void Reset() { } + + public void Dispose() { } +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/FrugalList.cs b/src/CommonUtilities/src/Collections/FrugalList.cs index be8d03f..e0556a4 100644 --- a/src/CommonUtilities/src/Collections/FrugalList.cs +++ b/src/CommonUtilities/src/Collections/FrugalList.cs @@ -57,7 +57,7 @@ namespace AnakinRaW.CommonUtilities.Collections; /// The type of elements in the list. [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] [DebuggerDisplay("Count = {Count}")] -public struct FrugalList : IList +public struct FrugalList : IList, IReadOnlyList { private static readonly EqualityComparer ItemComparer = EqualityComparer.Default; private static readonly EmptyList EmptyDummyList = EmptyList.Instance; @@ -65,13 +65,13 @@ public struct FrugalList : IList private T _firstItem = default!; private List? _tailList; - /// + /// public readonly int Count => _tailList is null ? 0 : 1 + _tailList.Count; /// - public readonly bool IsReadOnly => false; + readonly bool ICollection.IsReadOnly => false; - /// + /// public T this[int index] { readonly get @@ -106,6 +106,9 @@ public FrugalList(T item) /// /// The list whose elements are copied to the new list. /// is . + /// + /// Modifications to will not be reflected to this instance. + /// public FrugalList(IEnumerable collection) { if (collection == null) @@ -115,7 +118,7 @@ public FrugalList(IEnumerable collection) } /// - /// Initializes a new instance of the structure that copies all elements from the given list. + /// Initializes a new instance of the structure that contains elements copied from the specified list. /// /// The list whose elements are copied to the new list. /// @@ -136,9 +139,9 @@ public FrugalList(in FrugalList list) /// Modifications to this instance will not be reflected to the newly create readonly list. /// /// The read-only list. - public readonly ReadOnlyFrugalList AsReadOnly() + public readonly ImmutableFrugalList ToImmutableList() { - return new ReadOnlyFrugalList(in this); + return new ImmutableFrugalList(in this); } /// @@ -329,7 +332,7 @@ public readonly T[] ToArray() public readonly T First() { if (Count == 0) - throw new InvalidOperationException("The list contains no elements"); + throw new InvalidOperationException("The sequence contains no elements"); return _firstItem; } @@ -342,7 +345,7 @@ public readonly T Last() { var count = Count; if (count == 0) - throw new InvalidOperationException("The list contains no elements"); + throw new InvalidOperationException("The sequence contains no elements"); return count switch { 1 => _firstItem, @@ -378,12 +381,40 @@ public readonly T Last() /// /// Returns an enumerator that iterates through the /// - /// A for the . - public FrugalEnumerator GetEnumerator() => new(ref this); + /// A for the . + /// + /// + /// Important: Unlike standard .NET collection enumerators (e.g., ), + /// this enumerator does NOT detect modifications to the source collection and will NOT throw + /// when the collection is modified during enumeration. + /// + /// + /// Instead, the enumerator operates on a snapshot of the taken at the time + /// is called. Because is a , + /// the enumerator stores a copy of the list's state by value. + /// + /// + /// For predictable behavior, avoid modifying a while enumerating it. + /// If you need to modify the list during iteration, consider using a loop with an index, + /// or copy the list first using or . + /// + /// + /// The enumerator does not have exclusive access to the collection; therefore, enumerating through a collection is + /// intrinsically not a thread-safe procedure. To guarantee thread safety during enumeration, you can lock the collection + /// during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, + /// you must implement your own synchronization. + /// + /// + public Enumerator GetEnumerator() => new(ref this); - IEnumerator IEnumerable.GetEnumerator() => new FrugalEnumerator(ref this); + IEnumerator IEnumerable.GetEnumerator() + { + if (Count == 0) + return EmptyEnumerator.Instance; + return GetEnumerator(); + } - IEnumerator IEnumerable.GetEnumerator() => new FrugalEnumerator(ref this); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); /// /// Private type exists so that we can perform type checking on that type rather than reference checking. @@ -400,14 +431,45 @@ private EmptyList() : base(0) /// /// Enumerates the elements of a . /// - public struct FrugalEnumerator : IEnumerator + /// + /// + /// Important: Unlike standard .NET collection enumerators (e.g., ), + /// this enumerator does NOT detect modifications to the source collection and will NOT throw + /// when the collection is modified during enumeration. + /// + /// + /// Instead, the enumerator operates on a snapshot of the taken at the time + /// is called. Because is a , + /// the enumerator stores a copy of the list's state by value. + /// + /// + /// For predictable behavior, avoid modifying a while enumerating it. + /// If you need to modify the list during iteration, consider using a loop with an index, + /// or copy the list first using or . + /// + /// + /// The enumerator does not have exclusive access to the collection; therefore, enumerating through a collection is + /// intrinsically not a thread-safe procedure. To guarantee thread safety during enumeration, you can lock the collection + /// during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, + /// you must implement your own synchronization. + /// + /// + public struct Enumerator : IEnumerator { private readonly FrugalList _list; private int _position; private T _current; - readonly object IEnumerator.Current => Current!; + readonly object? IEnumerator.Current + { + get + { + if (_position <= 0) + throw new InvalidOperationException(); + return _current; + } + } /// public readonly T Current @@ -417,7 +479,7 @@ public readonly T Current } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal FrugalEnumerator(ref FrugalList list) + internal Enumerator(ref FrugalList list) { _list = list; _position = 0; @@ -428,13 +490,14 @@ internal FrugalEnumerator(ref FrugalList list) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - if (_position < _list.Count) + var localList = _list; + if ((uint)_position < (uint)localList.Count) { - _current = _list[_position]; + _current = localList[_position]; ++_position; return true; } - _position = _list.Count + 1; + _position = -1; _current = default!; return false; } @@ -444,7 +507,6 @@ public void Reset() { _current = default!; _position = 0; - } /// diff --git a/src/CommonUtilities/src/Collections/FrugalValueListDictionary.cs b/src/CommonUtilities/src/Collections/FrugalValueListDictionary.cs new file mode 100644 index 0000000..d6439ae --- /dev/null +++ b/src/CommonUtilities/src/Collections/FrugalValueListDictionary.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +#if NET6_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents a specialized dictionary that maps keys to lists of values, optimized for scenarios +/// where the number of values per key is expected to be one. +/// +/// The type of the keys in the dictionary. +/// The type of the values in the lists associated with the keys. +[DebuggerTypeProxy(typeof(IValueListDictionaryDebugView<,>))] +[DebuggerDisplay("Count = {Count}")] +public class FrugalValueListDictionary + : ValueListDictionaryBase>, IFrugalValueListDictionary + where TKey : notnull +{ + /// + public new ImmutableFrugalList this[TKey key] => GetValues(key); + + /// + /// Initializes a new instance of the class + /// that is empty and uses the specified . + /// + public FrugalValueListDictionary() + { + } + + /// + /// Initializes a new instance of the class + /// that is empty and uses the specified + /// + /// + /// The implementation to use when comparing keys, + /// or to use the default for the type of the key. + /// + /// + /// This constructor allows customization of how keys are compared in the dictionary. + /// If no equality comparer is provided, the default comparer for the key type is used. + /// + public FrugalValueListDictionary(IEqualityComparer? equalityComparer) : base(equalityComparer) + { + } + + /// + /// Creates a new instance of the value store specific to the implementation of the dictionary. + /// + /// + /// A new instance to be used as the value store for the dictionary. + /// + protected override FrugalList CreateValueStore() + { + return default; + } + + /// + public new ImmutableFrugalList GetValues(TKey key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) + return list.ToImmutableList(); + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + /// + public bool TryGetValues(TKey key, out ImmutableFrugalList values) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) + { + values = list.ToImmutableList(); + return true; + } + values = ImmutableFrugalList.Empty; + return false; + } + + /// + /// Creates a snapshot of the specified . + /// + /// The to create a snapshot from. + /// + /// An immutable, read-only list containing the elements of the specified . + /// + protected override IReadOnlyList CreateSnapshot(FrugalList list) + { + return list.ToImmutableList(); + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// An for the . + /// + /// + /// The enumerator returns each key exactly once, paired with a + /// containing all values associated with that key. + /// + /// + /// Enumerators can be used to read the data in the collection, but they cannot be used to modify + /// the underlying collection. + /// + /// + public new Enumerator GetEnumerator() => new(this, Enumerator.Frugal); + + /// + IEnumerator>> IReadOnlyFrugalValueListDictionary.GetEnumerator() + { + if (ValueCount == 0) + return EmptyEnumerator>>.Instance; + return GetEnumerator(); + } + + /// + IEnumerator>> IEnumerable>>.GetEnumerator() + { + if (ValueCount == 0) + return EmptyEnumerator>>.Instance; + return new Enumerator(this, Enumerator.AsReadOnly); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IReadOnlyValueListDictionary)this).GetEnumerator(); + } + + /// + /// Enumerates the elements of a . + /// + /// + /// + /// The enumerator provides a way to iterate through the key-value pairs in the dictionary, where each key is associated + /// with an containing all the values for that key. + /// + /// + /// The enumerator is a value type and does not allocate additional memory during enumeration. It is designed to be + /// efficient for scenarios where performance is critical. + /// + /// + /// Modifying the dictionary while enumerating through it will invalidate the enumerator, and any subsequent operation + /// on the enumerator will throw an . + /// + /// + public new struct Enumerator : + IEnumerator>>, + IEnumerator>> + { + private readonly FrugalValueListDictionary _dictionary; + private readonly int _getEnumeratorRetType; + + internal const int Frugal = 1; + internal const int AsReadOnly = 2; + + private readonly int _version; + private readonly int _count; + private int _index; + + private Entry _currentEntry; + + internal Enumerator(FrugalValueListDictionary dictionary, int getEnumeratorRetType) + { + _dictionary = dictionary; + _getEnumeratorRetType = getEnumeratorRetType; + _count = dictionary.KeyOrderStore.Count; + _index = 0; + _currentEntry = default; + _version = dictionary.Version; + } + + /// + public KeyValuePair> Current => _currentEntry.AsFrugal(); + + KeyValuePair> IEnumerator>>.Current + => _currentEntry.AsReadOnlyList(); + + object IEnumerator.Current + { + get + { + if (_index == 0 || _index == _count + 1) + throw new InvalidOperationException("Enumeration has either not started or has already finished."); + if (_getEnumeratorRetType == AsReadOnly) + return _currentEntry.AsReadOnlyList(); + return _currentEntry.AsFrugal(); + } + } + + /// + public bool MoveNext() + { + if (_version != _dictionary.Version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + + if ((uint)_index < (uint)_count) + { + var key = _dictionary.KeyOrderStore[_index]; + +#if NET6_0_OR_GREATER + ref var list = ref CollectionsMarshal.GetValueRefOrNullRef(_dictionary.ValueStore, key); + _currentEntry = new Entry(key, list.ToImmutableList()); +#else + _currentEntry = new Entry(key, _dictionary.ValueStore[key].ToImmutableList()); +#endif + _index++; + return true; + } + + _index = _count + 1; + _currentEntry = default; + return false; + } + + /// + public void Reset() + { + if (_version != _dictionary.Version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + + _index = 0; + _currentEntry = default; + } + + /// + public void Dispose() { } + } + + private readonly struct Entry(TKey key, ImmutableFrugalList values) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public KeyValuePair> AsFrugal() + { + return new KeyValuePair>(key, values); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public KeyValuePair> AsReadOnlyList() + { + return new KeyValuePair>(key, values); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/IFrugalValueListDictionary.cs b/src/CommonUtilities/src/Collections/IFrugalValueListDictionary.cs new file mode 100644 index 0000000..2f448ce --- /dev/null +++ b/src/CommonUtilities/src/Collections/IFrugalValueListDictionary.cs @@ -0,0 +1,29 @@ +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents a mutable generic collection that maps keys to lists of values, +/// using a memory-efficient representation optimized for one value per key. +/// +/// +/// +/// This interface combines the memory-efficient storage of +/// with the mutation capabilities of . +/// +/// +/// The underlying storage uses , which is optimized for cases where +/// most keys have zero or one value. +/// +/// +/// All methods returning return snapshots. +/// The returned lists are not affected by subsequent modifications to the dictionary. +/// +/// +/// The type of the keys in the dictionary. +/// The type of the values in the lists associated with the keys. +/// +/// +/// +public interface IFrugalValueListDictionary : + IReadOnlyFrugalValueListDictionary, + IValueListDictionary + where TKey : notnull; \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/IReadOnlyFrugalValueListDictionary.cs b/src/CommonUtilities/src/Collections/IReadOnlyFrugalValueListDictionary.cs new file mode 100644 index 0000000..b2033c3 --- /dev/null +++ b/src/CommonUtilities/src/Collections/IReadOnlyFrugalValueListDictionary.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents a read-only generic collection that maps keys to lists of values, +/// using a memory-efficient representation optimized for one value per key. +/// +/// +/// +/// This interface extends with +/// methods that return , a value type that provides +/// efficient storage for zero and one value. +/// +/// +/// All methods returning return snapshots. +/// The returned lists are not affected by subsequent modifications to the dictionary. +/// +/// +/// The type of the keys in the dictionary. +/// The type of the values in the lists associated with the keys. +public interface IReadOnlyFrugalValueListDictionary : + IReadOnlyValueListDictionary + where TKey : notnull +{ + /// + /// Gets the values associated with the specified key. + /// + /// The key whose values to get. + /// + /// An containing all values for the specified key. + /// + /// + /// The returned list is a snapshot; subsequent modifications to the dictionary + /// are not reflected in the returned list. + /// + /// is . + /// The key does not exist in the dictionary. + new ImmutableFrugalList this[TKey key] { get; } + + /// + /// Gets the values associated with the specified key. + /// + /// The key whose values to get. + /// + /// An containing all values for the specified key. + /// + /// + /// The returned list is a snapshot; subsequent modifications to the dictionary + /// are not reflected in the returned list. + /// + /// is . + /// The key does not exist in the dictionary. + new ImmutableFrugalList GetValues(TKey key); + + /// + /// Attempts to get the values associated with the specified key. + /// + /// The key whose values to get. + /// + /// When this method returns, contains an of values + /// associated with the specified key, if the key is found; otherwise, an empty list. + /// This parameter is passed uninitialized. + /// + /// + /// if the dictionary contains at least one value with the specified key; + /// otherwise, . + /// + /// + /// The returned list is a snapshot; subsequent modifications to the dictionary + /// are not reflected in the returned list. + /// + /// is . + bool TryGetValues(TKey key, out ImmutableFrugalList values); + + /// + /// Returns an enumerator that iterates through the dictionary. + /// + /// + /// An enumerator that yields key-value pairs where each value is an + /// of all values for that key. + /// + new IEnumerator>> GetEnumerator(); +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/IReadOnlyValueListDictionary.cs b/src/CommonUtilities/src/Collections/IReadOnlyValueListDictionary.cs index 132bcea..77146eb 100644 --- a/src/CommonUtilities/src/Collections/IReadOnlyValueListDictionary.cs +++ b/src/CommonUtilities/src/Collections/IReadOnlyValueListDictionary.cs @@ -5,30 +5,44 @@ namespace AnakinRaW.CommonUtilities.Collections; /// -/// Represents a read-only generic collection that maps keys to list of values. +/// Represents a generic read-only collection that maps keys to a list of values. /// /// /// /// Unlike a standard , this dictionary -/// allows multiple values to be associated with a single key. +/// allows multiple values to be associated with a single key using an +/// as the underlying value type. /// /// /// When enumerating, each key appears exactly once with all its associated values -/// as a . +/// stored to an . /// /// /// The type of keys in the dictionary. -/// The type of values in the dictionary. -public interface IReadOnlyValueListDictionary : IEnumerable>> where TKey : notnull +/// The type of the values in the lists associated with the keys. +public interface IReadOnlyValueListDictionary + : IEnumerable>> where TKey : notnull { /// /// Gets the list of values associated with the specified key. /// - /// The key of the values to get. - /// A containing all values for the specified key. + /// The key whose values to get. + /// + /// An containing all values for the specified key. + /// + /// + /// + /// Whether the returned list is a live view or a snapshot is implementation-defined. + /// Do not rely on the returned list reflecting subsequent modifications to the dictionary. + /// + /// + /// For consistent behavior across implementations, treat the returned list as valid + /// only until the next modification to the dictionary. + /// + /// /// is . /// The key does not exist in the dictionary. - ReadOnlyFrugalList this[TKey key] { get; } + IReadOnlyList this[TKey key] { get; } /// /// Gets a collection containing all values in the dictionary. @@ -43,7 +57,7 @@ public interface IReadOnlyValueListDictionary : IEnumerable /// - /// The collection count equals , not . + /// The collection count equals , not . /// Modifications to the returned collection are not reflected in the dictionary. /// /// @@ -63,18 +77,18 @@ public interface IReadOnlyValueListDictionary : IEnumerable Keys { get; } /// - /// Gets the total number of values across all keys in the dictionary. + /// Gets the number of distinct keys in the dictionary. /// - /// - /// This is the sum of all values for all keys, not the number of distinct keys. - /// Use to get the number of distinct keys. - /// int Count { get; } /// - /// Gets the number of distinct keys in the dictionary. + /// Gets the total number of values across all keys in the dictionary. /// - int KeyCount { get; } + /// + /// This is the sum of all values for all keys, not the number of distinct keys. + /// Use to get the number of distinct keys. + /// + int ValueCount { get; } /// /// Determines whether the dictionary contains the specified key. @@ -85,13 +99,25 @@ public interface IReadOnlyValueListDictionary : IEnumerable - /// Get a list of values stored with the specified key. + /// Gets the list of values associated with the specified key. /// - /// The key to get the list of values for. - /// The list of values of the specified . + /// The key whose values to get. + /// + /// + /// Whether the returned list is a live view or a snapshot is implementation-defined. + /// Do not rely on the returned list reflecting subsequent modifications to the dictionary. + /// + /// + /// For consistent behavior across implementations, treat the returned list as valid + /// only until the next modification to the dictionary. + /// + /// + /// + /// An containing all values for the specified key. + /// /// is . /// The key does not exist in the dictionary. - ReadOnlyFrugalList GetValues(TKey key); + IReadOnlyList GetValues(TKey key); /// /// Gets the last element with the specified key. @@ -134,13 +160,28 @@ public interface IReadOnlyValueListDictionary : IEnumerable - /// Gets the list of values associated with the specified key. + /// Attempts to get the list of values associated with the specified key. /// - /// The key whose value to get. + /// The key whose values to get. /// - /// When this method returns, a list of values associated with the specified key, if the key is found; - /// otherwise, an empty list. This parameter is passed uninitialized. - /// if the dictionary contains at least one value with the specified key; otherwise, . + /// When this method returns, contains an of values associated + /// with the specified key, if the key is found; otherwise, an empty read-only list. + /// This parameter is passed uninitialized. + /// + /// + /// + /// Whether the returned list is a live view or a snapshot is implementation-defined. + /// Do not rely on the returned list reflecting subsequent modifications to the dictionary. + /// + /// + /// For consistent behavior across implementations, treat the returned list as valid + /// only until the next modification to the dictionary. + /// + /// + /// + /// if the dictionary contains at least one value with the specified key; + /// otherwise, . + /// /// is . - bool TryGetValues(TKey key, out ReadOnlyFrugalList values); + bool TryGetValues(TKey key, out IReadOnlyList values); } \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/IValueListDictionary.cs b/src/CommonUtilities/src/Collections/IValueListDictionary.cs index 38da396..0141d8d 100644 --- a/src/CommonUtilities/src/Collections/IValueListDictionary.cs +++ b/src/CommonUtilities/src/Collections/IValueListDictionary.cs @@ -1,12 +1,13 @@ using System; +using System.Collections.Generic; namespace AnakinRaW.CommonUtilities.Collections; /// -/// Represents a generic collection that maps keys to list of values, while maintaining the order of key insertion. +/// Represents a generic collection that maps keys to a list of values, while maintaining the order of key insertion. /// /// The type of the keys in the dictionary. -/// The type of the value-list in the dictionary. +/// The type of the values in the lists associated with the keys. public interface IValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull { /// @@ -18,8 +19,8 @@ public interface IValueListDictionary : IReadOnlyValueListDictiona /// The key under which to add the value. /// The value to add. /// - /// if the key already existed and the value was added - /// to an existing key; if a new key was created. + /// if a new key is created; + /// otherwise, if the key already exists. /// /// is . bool Add(TKey key, TValue value); @@ -35,6 +36,10 @@ public interface IValueListDictionary : IReadOnlyValueListDictiona /// /// Removes a specific value associated with the specified key. /// + /// + /// If this was the last value for the key, the key is also removed from the dictionary. + /// If multiple identical values exist for the key, only the first occurrence is removed. + /// /// The key whose value to remove. /// The value to remove. /// @@ -42,18 +47,38 @@ public interface IValueListDictionary : IReadOnlyValueListDictiona /// if the key or value was not found. /// /// is . - /// - /// If this was the last value for the key, the key is also removed from the dictionary. - /// If multiple identical values exist for the key, only the first occurrence is removed. - /// bool Remove(TKey key, TValue value); /// /// Removes all keys and values from the . /// + void Clear(); + + /// + /// Adds multiple values under the specified key. + /// /// - /// and - /// are set to zero. + /// If is not empty, a new key is created if not already present. + /// Values are appended in enumeration order. /// - void Clear(); + /// The key under which to add the values. + /// The values to add. + /// + /// if a new key is created; + /// otherwise, if the key already exists, or is empty. + /// + /// or is . + bool AddRange(TKey key, IEnumerable values); + + /// + /// Removes all values that match the predicate from the specified key. + /// + /// + /// If all values for the key are removed, the key itself is also removed. + /// + /// The key whose values to filter. + /// The predicate that defines the conditions for removal. + /// The number of values removed. + /// or is . + int RemoveAll(TKey key, Predicate match); } \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs b/src/CommonUtilities/src/Collections/ImmutableFrugalList.cs similarity index 51% rename from src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs rename to src/CommonUtilities/src/Collections/ImmutableFrugalList.cs index 71ba813..123791b 100644 --- a/src/CommonUtilities/src/Collections/ReadOnlyFrugalList.cs +++ b/src/CommonUtilities/src/Collections/ImmutableFrugalList.cs @@ -11,58 +11,70 @@ namespace AnakinRaW.CommonUtilities.Collections; /// The type of elements in the list. [DebuggerTypeProxy(typeof(IReadOnlyCollectionDebugView<>))] [DebuggerDisplay("Count = {Count}")] -public readonly struct ReadOnlyFrugalList : IReadOnlyList +public readonly struct ImmutableFrugalList : IList, IReadOnlyList { /// - /// Returns an empty that has the specified type argument. + /// Returns an empty that has the specified type argument. /// - public static readonly ReadOnlyFrugalList Empty = default; + public static readonly ImmutableFrugalList Empty = default; private readonly FrugalList _list; - /// + /// public int Count => _list.Count; - /// + /// public T this[int index] => _list[index]; - + /// - /// Initializes a new instance of the structure to one item. + /// Initializes a new instance of the structure to one item. /// /// The item of the list. - public ReadOnlyFrugalList(T item) + internal ImmutableFrugalList(T item) { _list = new FrugalList(item); } /// - /// Initializes a new instance of the structure with the given enumerable. - /// - /// The items of this list. - public ReadOnlyFrugalList(IEnumerable items) - { - _list = new FrugalList(items); - } - - /// - /// Initializes a new instance of the structure from a . + /// Initializes a new instance of the structure from a . /// /// The items of this list. /// /// Modifications to will not be reflected to this instance. /// - internal ReadOnlyFrugalList(in FrugalList items) + internal ImmutableFrugalList(in FrugalList items) { _list = new FrugalList(in items); } - /// public void CopyTo(T[] array, int index) { _list.CopyTo(array, index); } + #region Explixit IList/ICollection implementations + + T IList.this[int index] + { + get => _list[index]; + set => throw new NotSupportedException("Collection is read-only."); + } + + bool ICollection.IsReadOnly => true; + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void IList.Insert(int index, T item) => throw new NotSupportedException("Collection is read-only."); + + bool ICollection.Remove(T item) => throw new NotSupportedException("Collection is read-only."); + + void IList.RemoveAt(int index) => throw new NotSupportedException("Collection is read-only."); + + void ICollection.Clear() => throw new NotSupportedException("Collection is read-only."); + + #endregion + #region Linq Re-Implemenations // Natively implementing frequent Linq functions avoids boxing. Add more if necessary. @@ -124,20 +136,20 @@ public T Last() } /// - /// Determines whether the contains a specific value. + /// Determines whether the contains a specific value. /// - /// The object to locate in the . - /// if is found in the ; otherwise, . + /// The object to locate in the . + /// if is found in the ; otherwise, . public bool Contains(T item) { return _list.Contains(item); } /// - /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire . + /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire . /// - /// The object to locate in the . The value can be for reference types. - /// The zero-based index of the first occurrence of within the entire , if found; otherwise, -1. + /// The object to locate in the . The value can be for reference types. + /// The zero-based index of the first occurrence of within the entire , if found; otherwise, -1. public int IndexOf(T item) { return _list.IndexOf(item); @@ -146,24 +158,65 @@ public int IndexOf(T item) #endregion /// - /// Returns an enumerator that iterates through the + /// Returns an enumerator that iterates through the immutable list. /// - /// A for the . - public FrugalList.FrugalEnumerator GetEnumerator() + /// An enumerator that can be used to iterate through the immutable list. + public FrugalList.Enumerator GetEnumerator() { // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable return _list.GetEnumerator(); } - /// + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// IEnumerator IEnumerable.GetEnumerator() { // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable - return _list.GetEnumerator(); + return Count == 0 + ? EmptyEnumerator.Instance + : _list.GetEnumerator(); } - IEnumerator IEnumerable.GetEnumerator() + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); +} + +/// +/// Provides static methods for immutable frugal lists. +/// +public static class ImmutableFrugalList +{ + /// + /// Creates a new instance of from the specified collection of items. + /// + /// The type of elements in the list. + /// The collection of items to initialize the list with. + /// + /// An containing the elements from the specified collection. + /// + /// is . + public static ImmutableFrugalList Create(IEnumerable items) { - return GetEnumerator(); + if (items is null) + throw new ArgumentNullException(nameof(items)); + + if (items is ImmutableFrugalList immutable) + return immutable; + if (items is FrugalList frugal) + return new ImmutableFrugalList(in frugal); + if (items is ICollection { Count: 0 }) + return ImmutableFrugalList.Empty; + if (items is IList { Count: 1 } list) + return new ImmutableFrugalList(list[0]); + return new FrugalList(items).ToImmutableList(); } } \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/ReadOnlyFrugalValueListDictionary.cs b/src/CommonUtilities/src/Collections/ReadOnlyFrugalValueListDictionary.cs new file mode 100644 index 0000000..3f20feb --- /dev/null +++ b/src/CommonUtilities/src/Collections/ReadOnlyFrugalValueListDictionary.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents a read-only dictionary that maps keys to immutable frugal lists of values. +/// +/// The type of the keys in the dictionary. +/// The type of the values in the dictionary. +[DebuggerTypeProxy(typeof(IValueListDictionaryDebugView<,>))] +[DebuggerDisplay("Count = {Count}")] +public class ReadOnlyFrugalValueListDictionary + : ReadOnlyValueListDictionaryBase, IReadOnlyFrugalValueListDictionary + where TKey : notnull +{ + private readonly IReadOnlyFrugalValueListDictionary _frugalValueList; + + /// Gets an empty . + /// An empty . + /// The returned instance is immutable and will always be empty. + public static ReadOnlyFrugalValueListDictionary Empty { get; } = + new(new FrugalValueListDictionary()); + + /// + public new ImmutableFrugalList this[TKey key] => GetValues(key); + + /// + /// Initializes a new instance of the class + /// that is a wrapper around the specified dictionary. + /// + /// The dictionary to wrap. + /// is . + public ReadOnlyFrugalValueListDictionary(IReadOnlyFrugalValueListDictionary dictionary) + : base(dictionary) + { + _frugalValueList = dictionary; + } + + /// + public new ImmutableFrugalList GetValues(TKey key) + { + return _frugalValueList.GetValues(key); + } + + /// + public bool TryGetValues(TKey key, out ImmutableFrugalList values) + { + return _frugalValueList.TryGetValues(key, out values); + } + + /// + public new IEnumerator>> GetEnumerator() + { + return _frugalValueList.GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionary.cs b/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionary.cs index b79df16..dde0736 100644 --- a/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionary.cs +++ b/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionary.cs @@ -1,156 +1,30 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; namespace AnakinRaW.CommonUtilities.Collections; /// -/// Represents a read-only, generic dictionary that maps keys to one or more values, while maintaining the order of key insertion. +/// Represents a read-only, generic dictionary that maps keys to a list of values. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. [DebuggerTypeProxy(typeof(IValueListDictionaryDebugView<,>))] [DebuggerDisplay("Count = {Count}")] -public class ReadOnlyValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull +public class ReadOnlyValueListDictionary : ReadOnlyValueListDictionaryBase + where TKey : notnull { - private readonly IValueListDictionary _dictionary; - /// Gets an empty . /// An empty . /// The returned instance is immutable and will always be empty. public static ReadOnlyValueListDictionary Empty { get; } = new(new ValueListDictionary()); - /// - public ReadOnlyFrugalList this[TKey key] => _dictionary[key]; - /// - /// Gets a key collection that contains the keys of the dictionary. - /// - public KeyCollection Keys => field ??= new KeyCollection(_dictionary.Keys); - - /// - /// Gets a collection that contains the values in the dictionary. - /// - public ValueCollection Values => field ??= new ValueCollection(_dictionary.Values); - - ICollection IReadOnlyValueListDictionary.Values => Values; - - ICollection IReadOnlyValueListDictionary.Keys => Keys; - - /// - public int Count => _dictionary.Count; - - /// - public int KeyCount => _dictionary.KeyCount; - - /// - /// Initializes a new instance of the class that is a wrapper around the specified value list dictionary. + /// Initializes a new instance of the class + /// that is a wrapper around the specified dictionary. /// /// The dictionary to wrap. /// is . - public ReadOnlyValueListDictionary(IValueListDictionary dictionary) - { - _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); - } - - /// - public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); - - /// - public ReadOnlyFrugalList GetValues(TKey key) => _dictionary.GetValues(key); - - /// - public TValue GetLastValue(TKey key) => _dictionary.GetLastValue(key); - - /// - public TValue GetFirstValue(TKey key) => _dictionary.GetFirstValue(key); - - /// - public bool TryGetFirstValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetFirstValue(key, out value); - - /// - public bool TryGetLastValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetLastValue(key, out value); - - /// - public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) => _dictionary.TryGetValues(key, out values); - - /// - public IEnumerator>> GetEnumerator() => _dictionary.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - /// Represents a read-only collection of the keys of a object. - /// - [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] - [DebuggerDisplay("Count = {Count}")] - public sealed class KeyCollection : ICollection, IReadOnlyCollection + public ReadOnlyValueListDictionary(IReadOnlyValueListDictionary dictionary) : base(dictionary) { - private readonly ICollection _collection; - - /// - public int Count => _collection.Count; - - bool ICollection.IsReadOnly => true; - - internal KeyCollection(ICollection collection) - { - _collection = collection ?? throw new ArgumentNullException(nameof(collection)); - } - - /// - public bool Contains(TKey item) => _collection.Contains(item); - - /// - public void CopyTo(TKey[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); - - /// - public IEnumerator GetEnumerator() => _collection.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); - - void ICollection.Add(TKey item) => throw new NotSupportedException(); - - void ICollection.Clear() => throw new NotSupportedException(); - - bool ICollection.Remove(TKey item) => throw new NotSupportedException(); - } - - /// - /// Represents a read-only collection of the values of a object. - /// - [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] - [DebuggerDisplay("Count = {Count}")] - public sealed class ValueCollection : ICollection, IReadOnlyCollection - { - private readonly ICollection _collection; - - /// - public int Count => _collection.Count; - - bool ICollection.IsReadOnly => true; - - internal ValueCollection(ICollection collection) - { - _collection = collection ?? throw new ArgumentNullException(nameof(collection)); - } - - bool ICollection.Contains(TValue item) => _collection.Contains(item); - - /// - public void CopyTo(TValue[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); - - /// - public IEnumerator GetEnumerator() => _collection.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); - - void ICollection.Add(TValue item) => throw new NotSupportedException(); - - void ICollection.Clear() => throw new NotSupportedException(); - - bool ICollection.Remove(TValue item) => throw new NotSupportedException(); } } \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionaryBase.cs b/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionaryBase.cs new file mode 100644 index 0000000..797f365 --- /dev/null +++ b/src/CommonUtilities/src/Collections/ReadOnlyValueListDictionaryBase.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Represents an abstract base class for a read-only, generic dictionary that maps keys to a list of values. +/// +/// The type of keys in the dictionary. +/// The type of values in the dictionary. +public abstract class ReadOnlyValueListDictionaryBase + : IReadOnlyValueListDictionary + where TKey : notnull +{ + private readonly IReadOnlyValueListDictionary _dictionary; + + /// + public IReadOnlyList this[TKey key] => _dictionary[key]; + + /// + /// Gets a key collection that contains the keys of the dictionary. + /// + public KeyCollection Keys => field ??= new KeyCollection(_dictionary.Keys); + + /// + /// Gets a collection that contains the values in the dictionary. + /// + public ValueCollection Values => field ??= new ValueCollection(_dictionary.Values); + + ICollection IReadOnlyValueListDictionary.Values => Values; + + ICollection IReadOnlyValueListDictionary.Keys => Keys; + + /// + public int ValueCount => _dictionary.ValueCount; + + /// + public int Count => _dictionary.Count; + + /// + /// Initializes a new instance of the class that is a wrapper around the specified value list dictionary. + /// + /// The dictionary to wrap. + /// is . + protected ReadOnlyValueListDictionaryBase(IReadOnlyValueListDictionary dictionary) + { + _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + } + + /// + public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); + + /// + public IReadOnlyList GetValues(TKey key) => _dictionary.GetValues(key); + + /// + public TValue GetLastValue(TKey key) => _dictionary.GetLastValue(key); + + /// + public TValue GetFirstValue(TKey key) => _dictionary.GetFirstValue(key); + + /// + public bool TryGetFirstValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetFirstValue(key, out value); + + /// + public bool TryGetLastValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetLastValue(key, out value); + + /// + public bool TryGetValues(TKey key, out IReadOnlyList values) => _dictionary.TryGetValues(key, out values); + + /// + public IEnumerator>> GetEnumerator() => _dictionary.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_dictionary).GetEnumerator(); + } + + /// + /// Represents a read-only collection of the keys of a object. + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("ValueCount = {Count}")] + public sealed class KeyCollection : ICollection, IReadOnlyCollection + { + private readonly ICollection _collection; + + /// + public int Count => _collection.Count; + + bool ICollection.IsReadOnly => true; + + internal KeyCollection(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + /// + public bool Contains(TKey item) => _collection.Contains(item); + + /// + public void CopyTo(TKey[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); + + /// + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + void ICollection.Add(TKey item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Remove(TKey item) => throw new NotSupportedException(); + } + + /// + /// Represents a read-only collection of the values of a object. + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("ValueCount = {Count}")] + public sealed class ValueCollection : ICollection, IReadOnlyCollection + { + private readonly ICollection _collection; + + /// + public int Count => _collection.Count; + + bool ICollection.IsReadOnly => true; + + internal ValueCollection(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + bool ICollection.Contains(TValue item) => _collection.Contains(item); + + /// + public void CopyTo(TValue[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); + + /// + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + void ICollection.Add(TValue item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Remove(TValue item) => throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/src/Collections/ValueListDictionary.cs b/src/CommonUtilities/src/Collections/ValueListDictionary.cs index 7bee636..cf689dc 100644 --- a/src/CommonUtilities/src/Collections/ValueListDictionary.cs +++ b/src/CommonUtilities/src/Collections/ValueListDictionary.cs @@ -1,20 +1,15 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -#if NET6_0_OR_GREATER -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -#endif namespace AnakinRaW.CommonUtilities.Collections; /// -/// Represents a generic dictionary that maps keys to one or more values, while maintaining the order of key insertion. +/// Represents a generic dictionary that maps keys to one or more values, +/// while maintaining the order of key insertion. /// /// The type of the keys in the dictionary. Keys must be non-nullable. -/// The type of the values in the dictionary. +/// The type of the values in the lists associated with the keys. /// /// /// Unlike a standard , this dictionary allows multiple values @@ -29,602 +24,94 @@ namespace AnakinRaW.CommonUtilities.Collections; /// [DebuggerTypeProxy(typeof(IValueListDictionaryDebugView<,>))] [DebuggerDisplay("Count = {Count}")] -public class ValueListDictionary : IValueListDictionary where TKey : notnull +public class ValueListDictionary + : ValueListDictionaryBase> + where TKey : notnull { - private readonly List _keyOrder = []; - private readonly Dictionary> _values; - - /// - public int Count { get; private set; } - - /// - public int KeyCount => _keyOrder.Count; - - /// - /// Gets a collection containing the keys in the . - /// - /// - /// A containing the keys in the . - /// - /// - /// - /// The keys in the are returned in the order they were first inserted. - /// - /// - /// The returned is not a static copy; instead, the - /// refers back to the keys in the original . - /// Therefore, changes to the continue to be - /// reflected in the . - /// - /// - public KeyCollection Keys => field ??= new KeyCollection(this); - - /// - ICollection IReadOnlyValueListDictionary.Keys => field ??= new KeyCollection(this); - - /// - public ReadOnlyFrugalList this[TKey key] => GetValues(key); - /// - /// Gets a collection containing all values in the . - /// - /// - /// A containing all values in the . - /// - /// - /// - /// Returns a flattened collection of all values across all keys. - /// If a key has multiple values, each value appears separately in the collection. - /// - /// - /// Values appear in insertion order: first all values for the first key (in the order they were added), - /// then all values for the second key, and so on. - /// - /// - /// The collection count equals , not . - /// - /// - /// The returned is not a static copy; instead, it - /// refers back to the values in the original . - /// Therefore, changes to the continue to be - /// reflected in the . - /// - /// - /// To get values for a specific key without flattening, use . - /// - /// - public ValueCollection Values => field ??= new ValueCollection(this); - - /// - ICollection IReadOnlyValueListDictionary.Values => Values; - - /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// that is empty and uses the default equality comparer for the key type. /// - public ValueListDictionary() : this(null) + public ValueListDictionary() { } /// - /// Initializes a new instance of the class - /// that is empty and uses the specified . + /// Initializes a new instance of the class + /// that is empty and uses the specified /// - /// - /// The implementation to use when comparing keys, - /// or to use the default for the type of the key. + /// + /// The implementation to use when comparing keys, + /// or to use the default for the type of the key. /// - public ValueListDictionary(IEqualityComparer? comparer) - { - _values = new Dictionary>(comparer ?? EqualityComparer.Default); - } - - /// - public bool Add(TKey key, TValue value) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - - Count++; - -#if NET - ref var list = ref CollectionsMarshal.GetValueRefOrAddDefault(_values, key, out var exists); - list.Add(value); - - if (!exists) - _keyOrder.Add(key); - - return exists; -#else - if (_values.TryGetValue(key, out var list)) - { - list.Add(value); - _values[key] = list; - return true; - } - - _keyOrder.Add(key); - _values[key] = new FrugalList(value); - return false; -#endif - } - - /// - public bool Remove(TKey key) - { - if (_values.TryGetValue(key, out var list)) - { - Count -= list.Count; - _values.Remove(key); - _keyOrder.Remove(key); - return true; - } - - return false; - } - - /// - public bool Remove(TKey key, TValue value) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - -# if NET6_0_OR_GREATER - ref var list = ref CollectionsMarshal.GetValueRefOrNullRef(_values, key); - if (Unsafe.IsNullRef(ref list)) - return false; - - if (!list.Remove(value)) - return false; - - Count--; - - // If this was the last value, remove the key entirely - if (list.Count == 0) - { - _values.Remove(key); - _keyOrder.Remove(key); - } - - return true; -# else - if (!_values.TryGetValue(key, out var list)) - return false; - - if (!list.Remove(value)) - return false; - - Count--; - - // If this was the last value, remove the key entirely - if (list.Count == 0) - { - _values.Remove(key); - _keyOrder.Remove(key); - } - else - { - _values[key] = list; - } - - return true; -# endif - } - - /// - public void Clear() - { - _keyOrder.Clear(); - _values.Clear(); - Count = 0; - } - - /// - public bool ContainsKey(TKey key) => _values.ContainsKey(key); - - /// - public ReadOnlyFrugalList GetValues(TKey key) - { - if (_values.TryGetValue(key, out var list)) - return list.AsReadOnly(); - - throw new KeyNotFoundException($"The key '{key}' was not found."); - } - - /// - public TValue GetFirstValue(TKey key) - { - if (_values.TryGetValue(key, out var list)) - return list[0]; - - throw new KeyNotFoundException($"The key '{key}' was not found."); - } - - /// - public TValue GetLastValue(TKey key) - { - if (_values.TryGetValue(key, out var list)) -#if NETSTANDARD2_1_OR_GREATER || NET - return list[^1]; -#else - return list[list.Count - 1]; -#endif - - throw new KeyNotFoundException($"The key '{key}' was not found."); - } - - /// - public bool TryGetFirstValue(TKey key, [MaybeNullWhen(false)] out TValue value) - { - if (_values.TryGetValue(key, out var list)) - { - value = list[0]; - return true; - } - - value = default!; - return false; - } - - /// - public bool TryGetLastValue(TKey key, [MaybeNullWhen(false)] out TValue value) - { - if (_values.TryGetValue(key, out var list)) - { -#if NETSTANDARD2_1_OR_GREATER || NET - value = list[^1]; -#else - value = list[list.Count - 1]; -#endif - return true; - } - - value = default!; - return false; - } - - /// - public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) + /// + /// This constructor allows customization of how keys are compared in the dictionary. + /// If no equality comparer is provided, the default comparer for the key type is used. + /// + public ValueListDictionary(IEqualityComparer? equalityComparer) : base(equalityComparer) { - if (_values.TryGetValue(key, out var list)) - { - values = list.AsReadOnly(); - return true; - } - - values = ReadOnlyFrugalList.Empty; - return false; + } - + /// - /// Returns an enumerator that iterates through the . + /// Creates a new instance of the value store for the dictionary. /// - /// An for the . - /// - /// - /// The enumerator returns each key exactly once, paired with a - /// containing all values associated with that key. - /// - /// - /// Enumerators can be used to read the data in the collection, but they cannot be used to modify - /// the underlying collection. - /// - /// - public Enumerator GetEnumerator() => new(this); - - IEnumerator>> IEnumerable>>.GetEnumerator() - => GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + /// + /// A new instance of to be used as the value store for the dictionary. + /// + protected override IList CreateValueStore() + { + return new ReadOnlyCachingList(); + } /// - /// Enumerates the elements of a . + /// Creates a snapshot of the specified list, providing a read-only view of its current state. /// - /// - /// Each element is a where the key is unique - /// and the value is a containing all values for that key. - /// - public struct Enumerator : IEnumerator>> + /// The list from which to create the snapshot. + /// A read-only view of the specified list. + protected override IReadOnlyList CreateSnapshot(IList list) { - private readonly ValueListDictionary _dictionary; - private int _index; - private KeyValuePair> _current; - - internal Enumerator(ValueListDictionary dictionary) - { - _dictionary = dictionary; - _index = 0; - _current = default; - } - - /// - public KeyValuePair> Current => _current; - - /// - object IEnumerator.Current - { - get - { - if (_index == 0 || _index == _dictionary._keyOrder.Count + 1) - throw new InvalidOperationException("Enumeration has either not started or has already finished."); - return Current; - } - } - - /// - public bool MoveNext() - { - if (_index < _dictionary._keyOrder.Count) - { - var key = _dictionary._keyOrder[_index]; - _current = new KeyValuePair>( - key, - _dictionary._values[key].AsReadOnly()); - _index++; - return true; - } - - _index = _dictionary._keyOrder.Count + 1; - _current = default; - return false; - } - - /// - public void Reset() - { - _index = 0; - _current = default; - } - - /// - public void Dispose() { } + return ((ReadOnlyCachingList)list).GetReadOnlyView(); } /// - /// Represents the collection of keys in a . + /// Invoked after a value list associated with a specific key has been modified. /// /// - /// - /// The keys in the are returned in the order they were first inserted - /// into the . - /// - /// - /// The is not a static copy; instead, the - /// refers back to the keys in the original . - /// Therefore, changes to the continue to be - /// reflected in the . - /// + /// This method invalidates the cached read-only view of . /// - [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] - [DebuggerDisplay("Count = {Count}")] - public sealed class KeyCollection : ICollection, IReadOnlyCollection + /// + /// The key associated with the modified value list. + /// + /// + /// The list of values that has been modified. + /// + protected override void OnAfterValueListModified(TKey key, IList list) { - private readonly ValueListDictionary _dictionary; - - internal KeyCollection(ValueListDictionary dictionary) - { - _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); - } - - /// - /// Gets the number of elements contained in the . - /// - /// The number of elements contained in the . - public int Count => _dictionary._keyOrder.Count; - - /// - /// Gets a value indicating whether the is read-only. - /// - /// Always returns . - public bool IsReadOnly => true; - - /// - /// Determines whether the contains a specific key. - /// - /// The key to locate in the . - /// - /// if is found in the ; - /// otherwise, . - /// - public bool Contains(TKey item) => _dictionary.ContainsKey(item); - - /// - public void CopyTo(TKey[] array, int arrayIndex) - { - if (array == null) - throw new ArgumentNullException(nameof(array)); - if (arrayIndex < 0) - throw new ArgumentOutOfRangeException(nameof(arrayIndex)); - if (array.Length - arrayIndex < Count) - throw new ArgumentException("Destination array is not long enough."); - - _dictionary._keyOrder.CopyTo(array, arrayIndex); - } - - /// - /// Returns an enumerator that iterates through the . - /// - /// A for the . - public List.Enumerator GetEnumerator() => _dictionary._keyOrder.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - /// This operation is not supported on a read-only collection. - /// - /// Always thrown. - void ICollection.Add(TKey item) => throw new NotSupportedException(); - - /// - /// This operation is not supported on a read-only collection. - /// - /// Always thrown. - void ICollection.Clear() => throw new NotSupportedException(); - - /// - /// This operation is not supported on a read-only collection. - /// - /// Always thrown. - bool ICollection.Remove(TKey item) => throw new NotSupportedException(); + ((ReadOnlyCachingList)list).InvalidateCache(); } - + /// - /// Represents the collection of values in a . + /// Represents a specialized list that supports caching of its read-only view. /// /// - /// - /// The values in the are returned grouped by key, in the order - /// the keys were first inserted into the . - /// Within each key group, values appear in the order they were added. - /// - /// - /// The is not a static copy; instead, the - /// refers back to the values in the original . - /// Therefore, changes to the continue to be - /// reflected in the . - /// + /// This class is used internally by to manage + /// the storage of values associated with a key. It provides efficient caching of a read-only + /// view of the list to minimize redundant allocations and improve performance. /// - [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] - [DebuggerDisplay("Count = {Count}")] - public sealed class ValueCollection : ICollection, IReadOnlyCollection + internal sealed class ReadOnlyCachingList : List { - private readonly ValueListDictionary _dictionary; - - internal ValueCollection(ValueListDictionary dictionary) - { - _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); - } - - /// - public int Count => _dictionary.Count; - - /// - /// Gets a value indicating whether the is read-only. - /// - /// Always returns . - public bool IsReadOnly => true; - - /// - public bool Contains(TValue item) - { - var comparer = EqualityComparer.Default; - foreach (var value in this) - { - if (comparer.Equals(value, item)) - return true; - } - return false; - } - - /// - public void CopyTo(TValue[] array, int arrayIndex) + private ReadOnlyCollection? _cachedReadOnly; + + internal ReadOnlyCollection GetReadOnlyView() { - if (array == null) - throw new ArgumentNullException(nameof(array)); - if (arrayIndex < 0) - throw new ArgumentOutOfRangeException(nameof(arrayIndex)); - if (array.Length - arrayIndex < Count) - throw new ArgumentException("Destination array is not long enough."); - - foreach (var value in this) - array[arrayIndex++] = value; + return _cachedReadOnly ??= new ReadOnlyCollection(this); } - /// - /// Returns an enumerator that iterates through the . - /// - /// An for the . - public Enumerator GetEnumerator() => new(_dictionary); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - /// This operation is not supported on a read-only collection. - /// - /// Always thrown. - void ICollection.Add(TValue item) => throw new NotSupportedException(); - - /// - /// This operation is not supported on a read-only collection. - /// - /// Always thrown. - void ICollection.Clear() => throw new NotSupportedException(); - - /// - /// This operation is not supported on a read-only collection. - /// - /// Always thrown. - bool ICollection.Remove(TValue item) => throw new NotSupportedException(); - - /// - /// Enumerates the elements of a . - /// - public struct Enumerator : IEnumerator + internal void InvalidateCache() { - private ValueListDictionary.Enumerator _dictEnumerator; - private FrugalList.FrugalEnumerator _valueEnumerator; - private bool _hasCurrentList; - - internal Enumerator(ValueListDictionary dictionary) - { - _dictEnumerator = dictionary.GetEnumerator(); - _valueEnumerator = default; - _hasCurrentList = false; - } - - /// - /// Gets the element at the current position of the enumerator. - /// - /// The element in the at the current position of the enumerator. - public TValue Current => _valueEnumerator.Current; - - /// - object IEnumerator.Current => Current!; - - /// - /// Advances the enumerator to the next element of the . - /// - /// - /// if the enumerator was successfully advanced to the next element; - /// if the enumerator has passed the end of the collection. - /// - public bool MoveNext() - { - // Try next value in current list - if (_hasCurrentList && _valueEnumerator.MoveNext()) - return true; - - // Move to next key-value group - while (_dictEnumerator.MoveNext()) - { - _valueEnumerator = _dictEnumerator.Current.Value.GetEnumerator(); - _hasCurrentList = true; - - if (_valueEnumerator.MoveNext()) - return true; - } - - return false; - } - - /// - /// Sets the enumerator to its initial position, which is before the first element in the collection. - /// - public void Reset() - { - _dictEnumerator.Reset(); - _valueEnumerator = default; - _hasCurrentList = false; - } - - /// - /// Releases all resources used by the . - /// - public void Dispose() => _dictEnumerator.Dispose(); + _cachedReadOnly = null; } } -} \ No newline at end of file +} + diff --git a/src/CommonUtilities/src/Collections/ValueListDictionaryBase.cs b/src/CommonUtilities/src/Collections/ValueListDictionaryBase.cs new file mode 100644 index 0000000..a8429dc --- /dev/null +++ b/src/CommonUtilities/src/Collections/ValueListDictionaryBase.cs @@ -0,0 +1,893 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +#if NET6_0_OR_GREATER +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#endif + +namespace AnakinRaW.CommonUtilities.Collections; + +/// +/// Provides a base class for a generic collection that maps keys to lists of values, +/// while maintaining the order of key insertion. +/// +/// The type of the keys in the dictionary. +/// The type of the values stored in the lists associated with each key. +/// The type of the list used to store values for each key. +/// +/// This class serves as an abstract base for collections that associate keys with multiple values stored in lists. +/// It ensures that keys are unique and maintains the order of value insertion within each list. +/// +public abstract class ValueListDictionaryBase : IValueListDictionary + where TKey : notnull + where TList : IList +{ + private int _version; + + /// + /// Gets the list of Keys present in the + /// with their insertion order. + /// + protected readonly List KeyOrderStore = []; + + /// + /// Gets the dictionary that stores the mutable value lists associated lists mapped to their associated keys. + /// + protected readonly Dictionary ValueStore; + + /// + /// Gets the version of the dictionary that is used for recognizing dictionary modifications. + /// + // ReSharper disable once ConvertToAutoPropertyWhenPossible + // Do not convert to auto-property, so we still can benefit from direct field access when writing the value + protected int Version => _version; + + /// + public IReadOnlyList this[TKey key] => GetValues(key); + + /// + public int ValueCount { get; private set; } + + /// + public int Count => KeyOrderStore.Count; + + /// + /// Gets a collection containing all values in the . + /// + /// + /// A containing all values in the . + /// + /// + /// + /// Returns a flattened collection of all values across all keys. + /// If a key has multiple values, each value appears separately in the collection. + /// + /// + /// Values appear in insertion order: first all values for the first key (in the order they were added), + /// then all values for the second key, and so on. + /// + /// + /// The collection count equals , not . + /// + /// + /// The returned is not a static copy; instead, it + /// refers back to the values in the original . + /// Therefore, changes to the continue to be + /// reflected in the . + /// + /// + /// To get values for a specific key without flattening, use . + /// + /// + public ValueCollection Values => field ??= new ValueCollection(this); + + /// + ICollection IReadOnlyValueListDictionary.Values => Values; + + /// + /// Gets a collection containing the keys in the . + /// + /// + /// A containing the keys in the . + /// + /// + /// + /// The keys in the are returned in the order they were first inserted. + /// + /// + /// The returned is not a static copy; instead, the + /// refers back to the keys in the original . + /// Therefore, changes to the continue to be + /// reflected in the . + /// + /// + public KeyCollection Keys => field ??= new KeyCollection(this); + + ICollection IReadOnlyValueListDictionary.Keys => Keys; + + + /// + /// Initializes a new instance of the class + /// that is empty and uses the default equality comparer for the key type. + /// + protected ValueListDictionaryBase() : this(null) + { + } + + /// + /// Initializes a new instance of the class + /// that is empty and uses the specified . + /// + /// + /// The implementation to use when comparing keys, + /// or to use the default for the type of the key. + /// + protected ValueListDictionaryBase(IEqualityComparer? comparer) + { + ValueStore = new Dictionary(comparer ?? EqualityComparer.Default); + } + + /// + /// Creates a new instance of the value store specific to the derived dictionary implementation. + /// + /// + /// A new instance of , which represents the collection of values + /// associated with a key in the dictionary. + /// + /// + /// This method is abstract and must be implemented by derived classes to provide the specific + /// type of value store used by the dictionary. + /// + protected abstract TList CreateValueStore(); + + /// + /// Creates a snapshot of the specified list, providing a read-only view of its current state. + /// + /// The list from which to create the snapshot. + /// A read-only list containing the current elements of the specified list. + protected abstract IReadOnlyList CreateSnapshot(TList list); + + /// + /// Invoked after a value list associated with a specific key has been modified. + /// + /// The key associated with the modified value list. + /// The value list that has been modified. + protected virtual void OnAfterValueListModified(TKey key, TList list) + { + } + + /// + public bool ContainsKey(TKey key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + return ValueStore.ContainsKey(key); + } + + /// + public IReadOnlyList GetValues(TKey key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) + return CreateSnapshot(list); + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + /// + public TValue GetLastValue(TKey key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) +#if NETSTANDARD2_1_OR_GREATER || NET + return list[^1]; +#else + return list[list.Count - 1]; +#endif + + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + /// + public TValue GetFirstValue(TKey key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) + return list[0]; + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + /// + public bool TryGetFirstValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) + { + value = list[0]; + return true; + } + + value = default!; + return false; + } + + /// + public bool TryGetLastValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) + { +#if NETSTANDARD2_1_OR_GREATER || NET + value = list[^1]; +#else + value = list[list.Count - 1]; +#endif + return true; + } + + value = default!; + return false; + } + + /// + public bool TryGetValues(TKey key, out IReadOnlyList values) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) + { + values = CreateSnapshot(list); + return true; + } + + values = []; + return false; + } + + /// + public bool Add(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + ValueCount++; + +#if NET6_0_OR_GREATER + ref var valueList = ref CollectionsMarshal.GetValueRefOrAddDefault(ValueStore, key, out var exists); + if (!exists) + { + valueList = CreateValueStoreInternal(); + KeyOrderStore.Add(key); + _version++; + } + Debug.Assert(valueList is not null); + + valueList.Add(value); + OnAfterValueListModified(key, valueList); + + return !exists; +#else + var exists = ValueStore.TryGetValue(key, out var valueList); + if (!exists) + { + valueList = CreateValueStoreInternal(); + KeyOrderStore.Add(key); + _version++; + } + + valueList!.Add(value); + OnAfterValueListModified(key, valueList); + + if (typeof(TList).IsValueType || !exists) + ValueStore[key] = valueList; + return !exists; +#endif + } + + + private TList CreateValueStoreInternal() + { + return CreateValueStore() ?? throw new InvalidOperationException("value store cannot be null"); + } + + /// + public bool Remove(TKey key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (ValueStore.TryGetValue(key, out var list)) + { + ValueCount -= list.Count; + ValueStore.Remove(key); + KeyOrderStore.Remove(key); + _version++; + return true; + } + + return false; + } + + /// + public bool Remove(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + +#if NET6_0_OR_GREATER + ref var list = ref CollectionsMarshal.GetValueRefOrNullRef(ValueStore, key); + if (Unsafe.IsNullRef(ref list)) + return false; + + if (!list.Remove(value)) + return false; + + ValueCount--; + OnAfterValueListModified(key, list); + + if (list.Count == 0) + { + ValueStore.Remove(key); + KeyOrderStore.Remove(key); + _version++; + } + + return true; +#else + if (!ValueStore.TryGetValue(key, out var list)) + return false; + + if (!list.Remove(value)) + return false; + + ValueCount--; + OnAfterValueListModified(key, list); + + if (list.Count == 0) + { + ValueStore.Remove(key); + KeyOrderStore.Remove(key); + _version++; + } + else if (typeof(TList).IsValueType) + { + ValueStore[key] = list; + } + + return true; +#endif + } + + /// + public void Clear() + { + if (KeyOrderStore.Count > 0) + { + KeyOrderStore.Clear(); + ValueStore.Clear(); + ValueCount = 0; + _version++; + } + } + + /// + public bool AddRange(TKey key, IEnumerable values) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (values == null) + throw new ArgumentNullException(nameof(values)); + + using var enumerator = values.GetEnumerator(); + if (!enumerator.MoveNext()) + return false; // Empty collection, nothing to add + +#if NET6_0_OR_GREATER + ref var valueList = ref CollectionsMarshal.GetValueRefOrAddDefault(ValueStore, key, out var exists); + if (!exists) + { + valueList = CreateValueStoreInternal(); + KeyOrderStore.Add(key); + _version++; + } + + Debug.Assert(valueList is not null); + + var countBefore = valueList.Count; + + valueList.Add(enumerator.Current); + while (enumerator.MoveNext()) + { + valueList.Add(enumerator.Current); + } + + var added = valueList.Count - countBefore; + ValueCount += added; + OnAfterValueListModified(key, valueList); + return !exists; +#else + var exists = ValueStore.TryGetValue(key, out var valueList); + if (!exists) + { + valueList = CreateValueStoreInternal(); + KeyOrderStore.Add(key); + _version++; + } + + var countBefore = valueList!.Count; + + valueList.Add(enumerator.Current); + while (enumerator.MoveNext()) + { + valueList.Add(enumerator.Current); + } + + var added = valueList.Count - countBefore; + ValueCount += added; + OnAfterValueListModified(key, valueList); + + if (typeof(TList).IsValueType || !exists) + ValueStore[key] = valueList; + + return !exists; +#endif + } + + /// + public int RemoveAll(TKey key, Predicate match) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (match == null) + throw new ArgumentNullException(nameof(match)); + +#if NET6_0_OR_GREATER + ref var list = ref CollectionsMarshal.GetValueRefOrNullRef(ValueStore, key); + if (Unsafe.IsNullRef(ref list)) + return 0; + + var removed = 0; + for (var i = list.Count - 1; i >= 0; i--) + { + if (match(list[i])) + { + list.RemoveAt(i); + removed++; + } + } + + if (removed > 0) + { + ValueCount -= removed; + OnAfterValueListModified(key, list); + + if (list.Count == 0) + { + ValueStore.Remove(key); + KeyOrderStore.Remove(key); + _version++; + } + } + + return removed; +#else + if (!ValueStore.TryGetValue(key, out var list)) + return 0; + + var removed = 0; + for (var i = list.Count - 1; i >= 0; i--) + { + if (match(list[i])) + { + list.RemoveAt(i); + removed++; + } + } + + if (removed > 0) + { + ValueCount -= removed; + OnAfterValueListModified(key, list); + + if (list.Count == 0) + { + ValueStore.Remove(key); + KeyOrderStore.Remove(key); + _version++; + } + else if (typeof(TList).IsValueType) + { + ValueStore[key] = list; + } + } + + return removed; +#endif + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// An for the . + /// + /// + /// The enumerator returns each key exactly once, paired with a + /// containing all values associated with that key. + /// + /// + /// Enumerators can be used to read the data in the collection, but they cannot be used to modify + /// the underlying collection. + /// + /// + public Enumerator GetEnumerator() => new(this); + + IEnumerator>> IEnumerable>>. + GetEnumerator() => ValueCount == 0 + ? EmptyEnumerator>>.Instance + : GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>>)this).GetEnumerator(); + + /// + /// Enumerates the elements of a . + /// + /// + /// Each element is a where the key is unique + /// and the value is a containing all values for that key. + /// + public struct Enumerator : IEnumerator>> + { + private readonly ValueListDictionaryBase _dictionary; + private readonly int _version; + private int _index; + private KeyValuePair> _current; + + internal Enumerator(ValueListDictionaryBase dictionary) + { + _dictionary = dictionary; + _version = dictionary.Version; + _index = 0; + _current = default; + } + + /// + public KeyValuePair> Current => _current; + + object IEnumerator.Current + { + get + { + if (_index == 0 || _index == _dictionary.Count + 1) + throw new InvalidOperationException("Enumeration has not started. Call MoveNext."); + return Current; + } + } + + /// + public bool MoveNext() + { + if (_version != _dictionary._version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + + var keyOrder = _dictionary.KeyOrderStore; + + if ((uint)_index < (uint)keyOrder.Count) + { + var key = keyOrder[_index++]; + var snapshot = _dictionary.CreateSnapshot(_dictionary.ValueStore[key]); + _current = new KeyValuePair>(key, snapshot); + return true; + } + + _index = keyOrder.Count + 1; + _current = default; + return false; + } + + /// + public void Reset() + { + if (_version != _dictionary._version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + + _index = 0; + _current = default; + } + + /// + public void Dispose() { } + } + + /// + /// Represents the collection of keys in a . + /// + /// + /// + /// The keys in the are returned in the order they were first inserted + /// into the . + /// + /// + /// The is not a static copy; instead, the + /// refers back to the keys in the original . + /// Therefore, changes to the continue to be + /// reflected in the . + /// + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("ValueCount = {Count}")] + public sealed class KeyCollection : ICollection, IReadOnlyCollection + { + private readonly ValueListDictionaryBase _dictionary; + + internal KeyCollection(ValueListDictionaryBase dictionary) + { + _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + } + + /// + /// Gets the number of elements contained in the . + /// + /// The number of elements contained in the . + public int Count => _dictionary.KeyOrderStore.Count; + + /// + /// Gets a value indicating whether the is read-only. + /// + /// Always returns . + public bool IsReadOnly => true; + + /// + /// Determines whether the contains a specific key. + /// + /// The key to locate in the . + /// + /// if is found in the ; + /// otherwise, . + /// + public bool Contains(TKey item) => _dictionary.ContainsKey(item); + + /// + public void CopyTo(TKey[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException(nameof(array)); + if (arrayIndex < 0) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + if (array.Length - arrayIndex < Count) + throw new ArgumentException("Destination array is not long enough."); + + _dictionary.KeyOrderStore.CopyTo(array, arrayIndex); + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// A for the . + public List.Enumerator GetEnumerator() => _dictionary.KeyOrderStore.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => Count == 0 + ? EmptyEnumerator.Instance + : GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + void ICollection.Add(TKey item) => throw new NotSupportedException(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + void ICollection.Clear() => throw new NotSupportedException(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + bool ICollection.Remove(TKey item) => throw new NotSupportedException(); + } + + /// + /// Represents the collection of values in a . + /// + /// + /// + /// The values in the are returned grouped by key, in the order + /// the keys were first inserted into the . + /// Within each key group, values appear in the order they were added. + /// + /// + /// The is not a static copy; instead, the + /// refers back to the values in the original . + /// Therefore, changes to the continue to be + /// reflected in the . + /// + /// + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("ValueCount = {Count}")] + public sealed class ValueCollection : ICollection, IReadOnlyCollection + { + private readonly ValueListDictionaryBase _dictionary; + + internal ValueCollection(ValueListDictionaryBase dictionary) + { + _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); + } + + /// + public int Count => _dictionary.ValueCount; + + /// + /// Gets a value indicating whether the is read-only. + /// + /// Always returns . + public bool IsReadOnly => true; + + /// + public bool Contains(TValue item) + { + foreach (var list in _dictionary.ValueStore.Values) + { + if (list.Contains(item)) + return true; + } + return false; + } + + /// + public void CopyTo(TValue[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException(nameof(array)); + if (arrayIndex < 0) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + if (array.Length - arrayIndex < Count) + throw new ArgumentException("Destination array is not long enough."); + + foreach (var value in this) + array[arrayIndex++] = value; + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// + /// An enumerator for the . + /// + public Enumerator GetEnumerator() => new(_dictionary); + + IEnumerator IEnumerable.GetEnumerator() + { + if (Count == 0) + return EmptyEnumerator.Instance; + return new Enumerator(_dictionary); + } + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + void ICollection.Add(TValue item) => throw new NotSupportedException(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + void ICollection.Clear() => throw new NotSupportedException(); + + /// + /// This operation is not supported on a read-only collection. + /// + /// Always thrown. + bool ICollection.Remove(TValue item) => throw new NotSupportedException(); + + /// + /// Enumerates the elements of a . + /// + /// + /// Enumerates the elements of a . + /// + public struct Enumerator : IEnumerator + { + private readonly ValueListDictionaryBase _dictionary; + private readonly int _version; + private TList? _currentList; + private int _keyIndex; + private int _valueIndex; + private TValue _current; + + internal Enumerator(ValueListDictionaryBase dictionary) + { + _dictionary = dictionary; + _currentList = default; + _keyIndex = 0; + _valueIndex = -1; + _current = default!; + _version = dictionary._version; + } + + /// + public TValue Current => _current; + + /// + object? IEnumerator.Current + { + get + { + if (_valueIndex < 0) + throw new InvalidOperationException("Enumeration has either not started or has already finished."); + return _current; + } + } + + /// + public bool MoveNext() + { + if (_version != _dictionary._version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + // Try to advance within current cached list + if (_valueIndex >= 0) + { + _valueIndex++; + if (_valueIndex < _currentList!.Count) + { + _current = _currentList[_valueIndex]; + return true; + } + // Current list exhausted, move to next key + _keyIndex++; + } + + // Find next non-empty list + return MoveToNextKey(); + } + + private bool MoveToNextKey() + { + var keyOrder = _dictionary.KeyOrderStore; + var values = _dictionary.ValueStore; + + if (_keyIndex < keyOrder.Count) + { + var key = keyOrder[_keyIndex]; + _currentList = values[key]; + + Debug.Assert(_currentList.Count > 0); + + _valueIndex = 0; + _current = _currentList[0]; + return true; + } + + _valueIndex = -1; + _current = default!; + return false; + } + + /// + public void Reset() + { + if (_version != _dictionary._version) + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + _currentList = default; + _keyIndex = 0; + _valueIndex = -1; + _current = default!; + } + + /// + public void Dispose() { } + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities/src/CommonUtilities.csproj b/src/CommonUtilities/src/CommonUtilities.csproj index bc19df5..41325ac 100644 --- a/src/CommonUtilities/src/CommonUtilities.csproj +++ b/src/CommonUtilities/src/CommonUtilities.csproj @@ -8,7 +8,7 @@ true - netstandard2.0;netstandard2.1;net8.0 + netstandard2.0;netstandard2.1;net10.0 AnakinRaW.CommonUtilities AnakinRaW.CommonUtilities en @@ -24,8 +24,8 @@ - - + + @@ -40,4 +40,8 @@ + + + + diff --git a/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs b/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs index 47aa8a3..72f9727 100644 --- a/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs +++ b/src/CommonUtilities/src/Hashing/Providers/HashAlgorithmProviderBase.cs @@ -85,7 +85,6 @@ protected async ValueTask ComputeHashAsyncWithHashAlgorithmLegacy(Stream so } } - protected int ComputeHashWithHashAlgorithmLegacy(Stream source, Span destination) { using var algorithm = CreateHashAlgorithm(); diff --git a/src/CommonUtilities/src/TaskExtensions.cs b/src/CommonUtilities/src/TaskExtensions.cs index e1505e4..895536d 100644 --- a/src/CommonUtilities/src/TaskExtensions.cs +++ b/src/CommonUtilities/src/TaskExtensions.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -#if !NET6_0_OR_GREATER +#if !NE10_0_OR_GREATER using System; using System.Runtime.InteropServices; using System.Threading; @@ -20,7 +20,7 @@ public static void Forget(this Task? task) { } -#if !NET6_0_OR_GREATER +#if !NE10_0_OR_GREATER /// /// Gets a that will complete when this completes diff --git a/src/CommonUtilities/test/AwaitExtensionsTests.cs b/src/CommonUtilities/test/AwaitExtensionsTests.cs index 19bf093..8b4e79b 100644 --- a/src/CommonUtilities/test/AwaitExtensionsTests.cs +++ b/src/CommonUtilities/test/AwaitExtensionsTests.cs @@ -58,15 +58,7 @@ public async Task WaitForExitAsync_UnstartedProcess() [Fact] public async Task WaitForExitAsync_DoesNotCompleteTillKilled() { - var processStartInfo = new ProcessStartInfo - { - FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/bin/bash", - Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "/c pause" : "-c read", - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardInput = true, - }; - var p = System.Diagnostics.Process.Start(processStartInfo)!; + var p = System.Diagnostics.Process.Start(CreateBlockingProcessStartInfo())!; var expectedExitCode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? -1 : 128 + 9; // https://stackoverflow.com/a/1041309 try @@ -95,15 +87,7 @@ public async Task WaitForExitAsync_DoesNotCompleteTillKilled() [Fact] public async Task WaitForExitAsync_Canceled() { - var processStartInfo = new ProcessStartInfo - { - FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/bin/bash", - Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "/c pause" : "-c read", - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardInput = true, - }; - var p = System.Diagnostics.Process.Start(processStartInfo)!; + var p = System.Diagnostics.Process.Start(CreateBlockingProcessStartInfo())!; try { var cts = new CancellationTokenSource(); @@ -117,4 +101,26 @@ public async Task WaitForExitAsync_Canceled() p.Kill(); } } + + private static ProcessStartInfo CreateBlockingProcessStartInfo() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = "/c ping -n 300 127.0.0.1 > nul", + CreateNoWindow = true, + UseShellExecute = false, + }; + } + + return new ProcessStartInfo + { + FileName = "sleep", + Arguments = "300", + CreateNoWindow = true, + UseShellExecute = false, + }; + } } \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/DebugViewTest.cs b/src/CommonUtilities/test/Collections/DebugViewTest.cs index 5cf3b4e..92afaac 100644 --- a/src/CommonUtilities/test/Collections/DebugViewTest.cs +++ b/src/CommonUtilities/test/Collections/DebugViewTest.cs @@ -17,7 +17,9 @@ public class DebugViewTests public static IEnumerable TestDebuggerAttributes_ValueListDictionaryInput() { yield return [new ValueListDictionary(), Array.Empty>()]; + yield return [new FrugalValueListDictionary(), Array.Empty>()]; yield return [new ReadOnlyValueListDictionary(new ValueListDictionary()), Array.Empty>()]; + yield return [new ReadOnlyFrugalValueListDictionary(new FrugalValueListDictionary()), Array.Empty>()]; yield return [ @@ -28,7 +30,15 @@ public static IEnumerable TestDebuggerAttributes_ValueListDictionaryIn new ("[2]", "Count = 1"), } ]; - + yield return + [ + new FrugalValueListDictionary{{1, "One"}, {2, "Two"}, {1, " Three"}}, + new KeyValuePair[] + { + new ("[1]", "Count = 2"), + new ("[2]", "Count = 1"), + } + ]; yield return [ new ReadOnlyValueListDictionary(new ValueListDictionary{{1, "One"}, {2, "Two"}, {1, " Three"}}), @@ -38,6 +48,15 @@ public static IEnumerable TestDebuggerAttributes_ValueListDictionaryIn new ("[2]", "Count = 1"), } ]; + yield return + [ + new ReadOnlyFrugalValueListDictionary(new FrugalValueListDictionary{{1, "One"}, {2, "Two"}, {1, " Three"}}), + new KeyValuePair[] + { + new ("[1]", "Count = 2"), + new ("[2]", "Count = 1"), + } + ]; } public static IEnumerable TestDebuggerAttributes_FrugalListsInput() @@ -45,8 +64,8 @@ public static IEnumerable TestDebuggerAttributes_FrugalListsInput() yield return [new FrugalList()]; yield return [new FrugalList { 1, 2 }]; - yield return [new ReadOnlyFrugalList()]; - yield return [new ReadOnlyFrugalList([1,2])]; + yield return [new ImmutableFrugalList()]; + yield return [new ImmutableFrugalList([1,2])]; } public static IEnumerable TestDebuggerAttributes_Inputs() diff --git a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTest.cs b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTest.cs index 0f32488..9a69bb7 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTest.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTest.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; + // ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; @@ -15,6 +17,26 @@ protected override string CreateT(int seed) } } +public class List_Generic_Tests_string_Immutable : FrugalListTest_String +{ + protected override bool IsReadOnly => true; + + protected override IList GenericIListFactory(int setLength) + { + return GenericFrugalListFactory(setLength).ToImmutableList(); + } + + protected override IList GenericIListFactory() + { + return GenericFrugalListFactory().ToImmutableList(); + } + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) + { + return new List(); + } +} + public class FrugalListTest_Int : FrugalListTestBase { protected override int CreateT(int seed) @@ -22,4 +44,25 @@ protected override int CreateT(int seed) var rand = new Random(seed); return rand.Next(); } -} \ No newline at end of file +} + +public class List_Generic_Tests_int_Immutable : FrugalListTest_Int +{ + protected override bool IsReadOnly => true; + + protected override IList GenericIListFactory(int setLength) + { + return GenericFrugalListFactory(setLength).ToImmutableList(); + } + + protected override IList GenericIListFactory() + { + return GenericFrugalListFactory().ToImmutableList(); + } + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) + { + return new List(); + } +} + diff --git a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs index e91cd84..bdd7ae9 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestBase.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing.Collections; using Xunit; namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; @@ -10,9 +9,11 @@ namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; /// /// Contains tests that ensure the correctness of the class. /// -public abstract class FrugalListTestBase : IListTestSuite +public abstract class FrugalListTestBase : FrugalListTestSuite { protected override bool Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + + protected override bool Enumerator_Empty_UsesSingletonInstance => true; protected override IList GenericIListFactory() { @@ -24,12 +25,12 @@ protected override IList GenericIListFactory(int count) return GenericFrugalListFactory(count); } - private static FrugalList GenericFrugalListFactory() + protected static FrugalList GenericFrugalListFactory() { return []; } - private FrugalList GenericFrugalListFactory(int count) + protected FrugalList GenericFrugalListFactory(int count) { var toCreateFrom = CreateEnumerable(null, count, 0, 0); return new FrugalList(toCreateFrom); @@ -44,7 +45,7 @@ public void Struct_Default() #pragma warning disable xUnit2013 Assert.Equal(0, list.Count); #pragma warning restore xUnit2013 - Assert.False(list.IsReadOnly); + Assert.False(((IList)list).IsReadOnly); } [Fact] @@ -55,7 +56,7 @@ public void Constructor_Empty() #pragma warning disable xUnit2013 Assert.Equal(0, list.Count); #pragma warning restore xUnit2013 - Assert.False(list.IsReadOnly); + Assert.False(((IList)list).IsReadOnly); } [Fact] @@ -68,7 +69,7 @@ public void Constructor_Single() Assert.Equal(1, list.Count); #pragma warning restore xUnit2013 Assert.Equal(t, list[0]); - Assert.False(list.IsReadOnly); + Assert.False(((IList)list).IsReadOnly); } [Theory] @@ -102,7 +103,7 @@ public void Constructor_IEnumerable(int _, int enumerableLength, int __, int num for (var i = 0; i < enumerableLength; i++) Assert.Equal(enumerable[i], list[i]); //"Expected object in item array to be the same as in the list" - Assert.False(list.IsReadOnly); //"List should not be readonly" + Assert.False(((IList)list).IsReadOnly); //"List should not be readonly" } [Theory] diff --git a/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestSuite.cs b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestSuite.cs new file mode 100644 index 0000000..bdb7ee2 --- /dev/null +++ b/src/CommonUtilities/test/Collections/FrugalList/FrugalListTestSuite.cs @@ -0,0 +1,11 @@ +using AnakinRaW.CommonUtilities.Testing.Collections; + +namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; + +public abstract class FrugalListTestSuite : IListTestSuite +{ + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; + protected override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; + protected override bool Enumerator_Empty_UsesSingletonInstance => true; +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTestBase.cs similarity index 59% rename from src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs rename to src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTestBase.cs index 025838e..05288eb 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTestBase.cs @@ -1,36 +1,95 @@ using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing.Collections; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Xunit; namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; /// -/// Contains tests that ensure the correctness of the class. +/// Contains tests that ensure the correctness of the class. /// -[SuppressMessage("ReSharper", "InconsistentNaming")] -public abstract class ReadOnlyFrugalListTestBase : IReadOnlyListTestSuite +public abstract class ImmutableFrugalListTestBase : FrugalListTestSuite { - protected virtual Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentException); + protected override bool IsReadOnly => true; - protected virtual ReadOnlyFrugalList GenericReadOnlyListFrugalListFactory(IEnumerable enumerable) + /// + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) { - return new ReadOnlyFrugalList(enumerable); + yield break; } - protected virtual ReadOnlyFrugalList GenericReadOnlyListFrugalListFactory(int count) + protected virtual ImmutableFrugalList GenericReadOnlyListFrugalListFactory(IEnumerable enumerable) + { + return ImmutableFrugalList.Create(enumerable); + } + + protected virtual ImmutableFrugalList GenericReadOnlyListFrugalListFactory(int count) { var baseCollection = CreateEnumerable(null, count, 0, 0); return GenericReadOnlyListFrugalListFactory(baseCollection); } - protected override IReadOnlyList GenericIReadOnlyListFactory(IEnumerable enumerable) + protected override IList GenericIListFactory() + { + return GenericReadOnlyListFrugalListFactory(0); + } + + protected override IList GenericIListFactory(int count) + { + return GenericReadOnlyListFrugalListFactory(count); + } + + #region ICollection{T}.IsReadOnly + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IsReadOnly_ReturnsTrue(int count) + { + ICollection list = GenericReadOnlyListFrugalListFactory(count); + Assert.True(list.IsReadOnly); + } + + #endregion + + #region Create{T} + + [Fact] + public void Create_NullArg_ThrowsArgumentNullException() + { + Assert.Throws("items", () => ImmutableFrugalList.Create(null!)); + } + + [Theory] + [MemberData(nameof(GetEnumerableTestData))] +#pragma warning disable xUnit1026 + public void Create_CreatesCorrectImmutableList(int _, int enumerableLength, int __, int ___) { - return GenericReadOnlyListFrugalListFactory(enumerable); + var list = CreateEnumerable(null, enumerableLength, 0, 0).ToList(); + + var listAsFrugal = new FrugalList(list); + var listAsImmutable = listAsFrugal.ToImmutableList(); + var listAsSet = list.ToHashSet(); + var listAsEnumerable = list.Where(_ => true); + + Assert.Equal(list, ImmutableFrugalList.Create(listAsFrugal)); + Assert.Equal(list, ImmutableFrugalList.Create(listAsImmutable)); + Assert.Equal(list, ImmutableFrugalList.Create(listAsSet)); + Assert.Equal(list, ImmutableFrugalList.Create(listAsEnumerable)); + + const ModifyOperation mods = ModifyOperation.Add | ModifyOperation.Insert | ModifyOperation.Overwrite | ModifyOperation.Remove | ModifyOperation.Clear; + + foreach (var modifyEnumerable in GetModifyEnumerables(mods, CreateT)) + { + var listCopy = new List(list); + var immutable = ImmutableFrugalList.Create(listCopy); + if (modifyEnumerable(listCopy)) + Assert.NotEqual(listCopy, immutable.ToList()); + } } +#pragma warning restore xUnit1026 + + #endregion #region Empty @@ -38,35 +97,43 @@ protected override IReadOnlyList GenericIReadOnlyListFactory(IEnumerable e public void Empty_Idempotent() { #pragma warning disable xUnit2002 - Assert.NotNull(ReadOnlyFrugalList.Empty); + Assert.NotNull(ImmutableFrugalList.Empty); #pragma warning restore xUnit2002 #pragma warning disable xUnit2013 - Assert.Equal(0, ReadOnlyFrugalList.Empty.Count); + Assert.Equal(0, ImmutableFrugalList.Empty.Count); #pragma warning restore xUnit2013 - Assert.Equal(ReadOnlyFrugalList.Empty, ReadOnlyFrugalList.Empty); + Assert.Equal(ImmutableFrugalList.Empty, ImmutableFrugalList.Empty); } #endregion #region Ctors - [Fact] - public void Ctor_NullList_ThrowsArgumentNullException() - { - Assert.Throws(() => new ReadOnlyFrugalList(null!)); - } - [Fact] public void Ctor_Single() { var t = CreateT(0); - var list = new ReadOnlyFrugalList(t); + // ReSharper disable once CollectionNeverUpdated.Local + var list = new ImmutableFrugalList(t); #pragma warning disable xUnit2013 Assert.Equal(1, list.Count); #pragma warning restore xUnit2013 Assert.Equal(t, list[0]); } + [Theory] + [MemberData(nameof(GetEnumerableTestData))] +#pragma warning disable xUnit1026 + public void Ctor_FrugalListIn(int _, int enumerableLength, int __, int numberOfDuplicateElements) + { + var enumerable = CreateEnumerable(null, enumerableLength, 0, numberOfDuplicateElements); + var frugal = new FrugalList(enumerable); + var immutable = new ImmutableFrugalList(in frugal); + + Assert.Equal(frugal, immutable); + } +#pragma warning restore xUnit1026 + [Theory] [MemberData(nameof(GetEnumerableTestData))] #pragma warning disable xUnit1026 @@ -78,9 +145,9 @@ public void Ctor_ModificationsGetNotReflectedWhenOriginalListChanges(int _, int var frugal = new FrugalList(enumerable); ref var refFrugal = ref frugal; - var roFrugal = new ReadOnlyFrugalList(in frugal); + var immutable = new ImmutableFrugalList(in frugal); - Assert.Equal(refFrugal.ToList(), roFrugal.ToList()); + Assert.Equal(refFrugal.ToList(), immutable.ToList()); if (enumerableLength == 0) return; @@ -89,9 +156,12 @@ public void Ctor_ModificationsGetNotReflectedWhenOriginalListChanges(int _, int var mods = ModifyOperation.Add | ModifyOperation.Insert | ModifyOperation.Overwrite | ModifyOperation.Remove | ModifyOperation.Clear; - foreach (var modifyEnumerable in IListTestSuite.GetModifyEnumerables(mods, CreateT)) - if (modifyEnumerable(asEnumerable)) - Assert.NotEqual(asEnumerable.ToList(), roFrugal.ToList()); + foreach (var modifyEnumerable in GetModifyEnumerables(mods, CreateT)) + { + var listCopy = new List(asEnumerable); + if (modifyEnumerable(listCopy)) + Assert.NotEqual(listCopy, immutable.ToList()); + } } #endregion @@ -182,77 +252,83 @@ public void Contains_ValidValueOnCollectionNotContainingThatValue(int count) var item = CreateT(seed++); while (collection.Contains(item)) item = CreateT(seed++); +#pragma warning disable xUnit2017 Assert.False(collection.Contains(item)); +#pragma warning restore xUnit2017 } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void Contains_ValidValueOnCollectionContainingThatValue(int count) { +#pragma warning disable xUnit2017 var collection = GenericReadOnlyListFrugalListFactory(count); foreach (var item in collection) Assert.True(collection.Contains(item)); +#pragma warning restore xUnit2017 } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void Contains_DefaultValueOnCollectionNotContainingDefaultValue(int count) { +#pragma warning disable xUnit2017 var collection = GenericReadOnlyListFrugalListFactory(count); if (default(T) is null) Assert.False(collection.Contains(default!)); +#pragma warning restore xUnit2017 } #endregion #region IndexOf - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IList_Generic_IndexOf_DefaultValueNotContainedInList(int count) - { - var list = GenericReadOnlyListFrugalListFactory(count); - var value = default(T); - if (list.Contains(value!)) - return; - Assert.Equal(-1, list.IndexOf(value!)); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IList_Generic_IndexOf_DefaultValueContainedInList(int count) - { - if (count > 0) - { - var list = GenericReadOnlyListFrugalListFactory(count); - var value = default(T); - if (!list.Contains(value!)) - return; - Assert.Equal(0, list.IndexOf(value!)); - } - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IList_Generic_IndexOf_ValidValueNotContainedInList(int count) - { - var list = GenericReadOnlyListFrugalListFactory(count); - var seed = 54321; - var value = CreateT(seed++); - while (list.Contains(value)) - value = CreateT(seed++); - Assert.Equal(-1, list.IndexOf(value)); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void IList_Generic_IndexOf_EachValueNoDuplicates(int count) - { - // Assumes no duplicate elements contained in the list returned by GenericIListFactory - var list = GenericReadOnlyListFrugalListFactory(count); - foreach (var i in Enumerable.Range(0, count)) - Assert.Equal(i, list.IndexOf(list[i])); - } + //[Theory] + //[MemberData(nameof(ValidCollectionSizes))] + //public void IList_Generic_IndexOf_DefaultValueNotContainedInList(int count) + //{ + // var list = GenericReadOnlyListFrugalListFactory(count); + // var value = default(T); + // if (list.Contains(value!)) + // return; + // Assert.Equal(-1, list.IndexOf(value!)); + //} + + //[Theory] + //[MemberData(nameof(ValidCollectionSizes))] + //public void IList_Generic_IndexOf_DefaultValueContainedInList(int count) + //{ + // if (count > 0) + // { + // var list = GenericReadOnlyListFrugalListFactory(count); + // var value = default(T); + // if (!list.Contains(value!)) + // return; + // Assert.Equal(0, list.IndexOf(value!)); + // } + //} + + //[Theory] + //[MemberData(nameof(ValidCollectionSizes))] + //public void IList_Generic_IndexOf_ValidValueNotContainedInList(int count) + //{ + // var list = GenericReadOnlyListFrugalListFactory(count); + // var seed = 54321; + // var value = CreateT(seed++); + // while (list.Contains(value)) + // value = CreateT(seed++); + // Assert.Equal(-1, list.IndexOf(value)); + //} + + //[Theory] + //[MemberData(nameof(ValidCollectionSizes))] + //public void IList_Generic_IndexOf_EachValueNoDuplicates(int count) + //{ + // // Assumes no duplicate elements contained in the list returned by GenericIListFactory + // var list = GenericReadOnlyListFrugalListFactory(count); + // foreach (var i in Enumerable.Range(0, count)) + // Assert.Equal(i, list.IndexOf(list[i])); + //} #endregion diff --git a/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTests.cs b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs similarity index 64% rename from src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTests.cs rename to src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs index 573bf82..3c56ac3 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/ReadOnlyFrugalListTests.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs @@ -1,12 +1,11 @@ using AnakinRaW.CommonUtilities.Collections; using System; -using System.Collections.Generic; using Xunit; // ReSharper disable InconsistentNaming namespace AnakinRaW.CommonUtilities.Test.Collections.FrugalList; -public class ReadOnlyFrugalListTest_String : ReadOnlyFrugalListTestBase +public class ImmutableFrugalListTestString : ImmutableFrugalListTestBase { protected override string CreateT(int seed) { @@ -18,7 +17,7 @@ protected override string CreateT(int seed) } } -public class ReadOnlyFrugalListTest_Int : ReadOnlyFrugalListTestBase +public class ImmutableFrugalListTestInt : ImmutableFrugalListTestBase { private static readonly int[] _intArray = [-4, 5, -2, 3, 1, 2, -1, -3, 0, 4, -5, 3, 3]; private static readonly int[] _excludedFromIntArray = [100, -34, 42, int.MaxValue, int.MinValue]; @@ -32,18 +31,20 @@ protected override int CreateT(int seed) [Fact] public static void Contains() { - var collection = new ReadOnlyFrugalList(_intArray); +#pragma warning disable xUnit2017 + var collection = ImmutableFrugalList.Create(_intArray); foreach (var item in _intArray) Assert.True(collection.Contains(item)); foreach (var excluded in _excludedFromIntArray) Assert.False(collection.Contains(excluded)); +#pragma warning restore xUnit2017 } [Fact] public static void IndexOf() { - var collection = new ReadOnlyFrugalList(_intArray); + var collection = ImmutableFrugalList.Create(_intArray); foreach (var item in _intArray) Assert.Equal(Array.IndexOf(_intArray, item), collection.IndexOf(item)); @@ -51,20 +52,4 @@ public static void IndexOf() foreach (var excluded in _excludedFromIntArray) Assert.Equal(-1, collection.IndexOf(excluded)); } -} - - -public class ReadOnlyFrugalListTest_Int_FromFrugal : ReadOnlyFrugalListTestBase -{ - protected override int CreateT(int seed) - { - var rand = new Random(seed); - return rand.Next(); - } - - protected override ReadOnlyFrugalList GenericReadOnlyListFrugalListFactory(IEnumerable enumerable) - { - var frugal = new FrugalList(enumerable); - return frugal.AsReadOnly(); - } } \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTest.Keys.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTest.Keys.cs new file mode 100644 index 0000000..8065396 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTest.Keys.cs @@ -0,0 +1,30 @@ +using System; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.Frugal; + +// ReSharper disable once InconsistentNaming +public class FrugalValueListDictionary_Keys : ValueListDictionary_Keys_TestSuite +{ + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory() + { + return (FrugalValueListDictionary)MutableValueListDictionaryFactory(); + } + + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory(IValueListDictionary dictionary) + { + return (FrugalValueListDictionary)dictionary; + } + + protected override IValueListDictionary MutableValueListDictionaryFactory() + { + return new FrugalValueListDictionary(); + } + + [Fact] + public void FrugalValueListDictionary_KeyCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new FrugalValueListDictionary.KeyCollection(null!)); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTest.Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTest.Values.cs new file mode 100644 index 0000000..b26012c --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTest.Values.cs @@ -0,0 +1,29 @@ +using AnakinRaW.CommonUtilities.Collections; +using System; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.Frugal; + +public class FrugalValueListDictionary_Values : ValueListDictionary_Values_TestSuite +{ + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory() + { + return (FrugalValueListDictionary)MutableValueListDictionaryFactory(); + } + + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory(IValueListDictionary dictionary) + { + return (FrugalValueListDictionary)dictionary; + } + + protected override IValueListDictionary MutableValueListDictionaryFactory() + { + return new FrugalValueListDictionary(); + } + + [Fact] + public void FrugalValueListDictionary_ValueCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new FrugalValueListDictionary.ValueCollection(null!)); + } +} diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTestBase.cs new file mode 100644 index 0000000..c38908b --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTestBase.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.Frugal; + +public abstract class FrugalValueListDictionaryTestBase + : ValueListDictionaryBaseTestSuite> + where TKey : notnull +{ + protected override bool ValueList_IsReadOnlyView => false; + + protected FrugalValueListDictionary FrugalValueListDictionaryFactory(IEqualityComparer? comparer = null) + { + return new FrugalValueListDictionary(comparer); + } + + protected FrugalValueListDictionary FrugalValueListDictionaryFactory(int count) + { + var dict = FrugalValueListDictionaryFactory(); + AddToCollection(dict, count); + return dict; + } + + protected override ValueListDictionaryBase> ValueListDictionaryFactory(int count) + { + return FrugalValueListDictionaryFactory(count); + } + + protected override ValueListDictionaryBase> ValueListDictionaryFactory( + IEqualityComparer? comparer = null) + { + return FrugalValueListDictionaryFactory(); + } + + #region Constructors + + [Fact] + public void Ctor_InitializesCorrectly() + { + var dict = new FrugalValueListDictionary(); + Assert.Equal(0, dict.ValueCount); + Assert.Equal(0, dict.Count); + } + + #endregion + + #region GetEnumerator + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetEnumerator_EnumeratesDictionaryCorrectly(int count) + { + var collection = FrugalValueListDictionaryFactory(count); + + using var enumerator1 = collection.GetEnumerator(); + using var enumerator2 = ((IReadOnlyFrugalValueListDictionary)collection).GetEnumerator(); + IEnumerator enumerator3 = collection.GetEnumerator(); + + foreach (var keyValuePair in collection) + { + Assert.True(enumerator1.MoveNext()); + Assert.True(enumerator2.MoveNext()); + Assert.True(enumerator3.MoveNext()); + + Assert.Equal(keyValuePair.Key, enumerator1.Current.Key); + Assert.Equal(keyValuePair.Key, enumerator2.Current.Key); + Assert.Equal(keyValuePair.Key, ((KeyValuePair>)enumerator3.Current).Key); + + Assert.Equal(keyValuePair.Value, enumerator1.Current.Value); + Assert.Equal(keyValuePair.Value, enumerator2.Current.Value); + Assert.Equal(keyValuePair.Value, ((KeyValuePair>)enumerator3.Current).Value); + } + Assert.False(enumerator1.MoveNext()); + + if (enumerator3 is IDisposable disposable) + disposable.Dispose(); + } + + #endregion + + #region Item Getter + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_ItemGet_DefaultKey(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary[default!]); + return; + } + + var value = CreateTValue(3452); + dictionary.Add(default!, value); + Assert.Equal(value, dictionary[default!].First()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_ItemGet_MissingNonDefaultKey_ThrowsKeyNotFoundException(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary[missingKey]); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_ItemGet_MissingDefaultKey_ThrowsKeyNotFoundException(int count) + { + if (DefaultValueAllowed) + { + var dictionary = FrugalValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + dictionary.Remove(missingKey); + Assert.Throws(() => dictionary[missingKey]); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_ItemGet_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + foreach (var pair in dictionary) + Assert.Equal(pair.Value, dictionary[pair.Key]); + } + + #endregion + + #region GetValues + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_GetValues_DefaultKey(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.GetValues(default!)); + return; + } + + var value = CreateTValue(3452); + dictionary.Add(default!, value); + Assert.Equal(value, dictionary.GetValues(default!).First()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_GetValues_MissingNonDefaultKey_ThrowsKeyNotFoundException(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary.GetValues(missingKey)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_GetValues_MissingDefaultKey_ThrowsKeyNotFoundException(int count) + { + if (DefaultValueAllowed) + { + var dictionary = FrugalValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + dictionary.Remove(missingKey); + Assert.Throws(() => dictionary.GetValues(missingKey)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_GetValues_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + foreach (var pair in dictionary) + Assert.Equal(pair.Value, dictionary.GetValues(pair.Key)); + } + + #endregion + + #region TryGetValues + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_TryGetValues_DefaultKey(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.TryGetValues(default!, out _)); + return; + } + + var first = CreateTValue(3452); + var second = CreateTValue(5431); + dictionary.Add(default!, first); + dictionary.Add(default!, second); + Assert.True(dictionary.TryGetValues(default!, out var valueList)); + Assert.Equal([first, second], valueList); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_TryGetValues_MissingNonDefaultKey_ReturnsFalseAndSetsDefault(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + var missingKey = GetNewKey(dictionary); + Assert.False(dictionary.TryGetValues(missingKey, out var valueList)); + Assert.Equal([], valueList); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_TryGetValues_MissingDefaultKey_ReturnsFalseAndSetsDefault(int count) + { + if (DefaultValueAllowed) + { + var dictionary = FrugalValueListDictionaryFactory(count); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + dictionary.Remove(missingKey); + Assert.False(dictionary.TryGetValues(missingKey, out var valueList)); + Assert.Equal([], valueList); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void FrugalValueListDictionary_TryGetValues_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = FrugalValueListDictionaryFactory(count); + foreach (var pair in dictionary) + { + Assert.True(dictionary.TryGetValues(pair.Key, out var valueList)); + Assert.Equal(pair.Value, valueList); + } + } + + #endregion +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTests.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTests.cs new file mode 100644 index 0000000..949f4d7 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/FrugalValueListDictionaryTests.cs @@ -0,0 +1,38 @@ +using System; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.Frugal; + +public class FrugalValueListDictionaryTest_int_int : FrugalValueListDictionaryTestBase +{ + protected override bool DefaultValueAllowed => true; + + protected override int CreateTKey(int seed) + { + var rand = new Random(seed); + return rand.Next(); + } + + protected override int CreateTValue(int seed) + { + return CreateTKey(seed); + } +} + +public class FrugalValueListDictionaryTest_string_string : FrugalValueListDictionaryTestBase +{ + protected override bool DefaultValueAllowed => false; + + protected override string CreateTKey(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + + protected override string CreateTValue(int seed) + { + return CreateTKey(seed); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTest.Keys.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTest.Keys.cs new file mode 100644 index 0000000..264b109 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTest.Keys.cs @@ -0,0 +1,30 @@ +using System; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.Frugal; + +// ReSharper disable once InconsistentNaming +public class ReadOnlyFrugalValueListDictionary_Keys : ValueListDictionary_Keys_TestSuite +{ + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory() + { + return new ReadOnlyFrugalValueListDictionary(new FrugalValueListDictionary()); + } + + protected override IValueListDictionary MutableValueListDictionaryFactory() + { + return new FrugalValueListDictionary(); + } + + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory(IValueListDictionary dictionary) + { + return new ReadOnlyFrugalValueListDictionary((IReadOnlyFrugalValueListDictionary)dictionary); + } + + [Fact] + public void ReadOnlyFrugalValueListDictionary_KeyCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ReadOnlyFrugalValueListDictionary.KeyCollection(null!)); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTest.Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTest.Values.cs new file mode 100644 index 0000000..64f2d23 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTest.Values.cs @@ -0,0 +1,32 @@ +using System; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.Frugal; + +// ReSharper disable once InconsistentNaming + +public class ReadOnlyFrugalValueListDictionary_Values : ValueListDictionary_Values_TestSuite +{ + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory() + { + return new ReadOnlyFrugalValueListDictionary(new FrugalValueListDictionary()); + } + + protected override IValueListDictionary MutableValueListDictionaryFactory() + { + return new FrugalValueListDictionary(); + } + + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory(IValueListDictionary dictionary) + { + return new ReadOnlyFrugalValueListDictionary((IReadOnlyFrugalValueListDictionary)dictionary); + } + + [Fact] + public void ReadOnlyFrugalValueListDictionary_ValueCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ReadOnlyFrugalValueListDictionary.ValueCollection(null!)); + } +} + diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTestBase.cs new file mode 100644 index 0000000..8f3fa41 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTestBase.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.Frugal; + +public abstract class ReadOnlyFrugalValueListDictionaryTestBase + : ReadOnlyValueListDictionaryBaseTestSuite + where TKey : notnull +{ + protected override bool DefaultValueAllowed => false; + + protected ReadOnlyFrugalValueListDictionary ReadOnlyFrugalValueListDictionaryFactory( + int count, out FrugalValueListDictionary mutableDict) + { + mutableDict = MutableFrugalValueListDictionaryFactory(); + AddToCollection(mutableDict, count); + return new ReadOnlyFrugalValueListDictionary(mutableDict); + } + + + protected ReadOnlyFrugalValueListDictionary ReadOnlyFrugalValueListDictionaryFactory( + IReadOnlyFrugalValueListDictionary dictionary) + { + return new ReadOnlyFrugalValueListDictionary(dictionary); + } + + protected sealed override ReadOnlyValueListDictionaryBase ReadOnlyValueListDictionaryFactory( + IReadOnlyValueListDictionary dictionary) + { + if (dictionary is not IReadOnlyFrugalValueListDictionary frugalValueList) + throw new InvalidOperationException("invalid test construction"); + return ReadOnlyFrugalValueListDictionaryFactory(frugalValueList); + } + + protected FrugalValueListDictionary MutableFrugalValueListDictionaryFactory() + { + return new FrugalValueListDictionary(); + } + + protected sealed override IValueListDictionary MutableValueListDictionaryFactory() + { + return MutableFrugalValueListDictionaryFactory(); + } + + protected override IEnumerable NonGenericIEnumerableFactory(int count) + { + return ReadOnlyFrugalValueListDictionaryFactory(count, out _); + } + + #region Ctor + + [Fact] + public void CtorTests_Negative() + { + AssertExtensions.Throws("dictionary", + () => _ = new ReadOnlyFrugalValueListDictionary(null!)); + } + + #endregion + + #region ReadOnlyFrugalValueListDictionary{TKey, TValue}.Empty + + [Fact] + public static void Empty_Idempotent() + { + Assert.NotNull(ReadOnlyFrugalValueListDictionary.Empty); + Assert.Equal(0, ReadOnlyFrugalValueListDictionary.Empty.ValueCount); + Assert.Same(ReadOnlyFrugalValueListDictionary.Empty, ReadOnlyFrugalValueListDictionary.Empty); + } + + #endregion + + #region GetEnumerator + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetEnumerator(int count) + { + // ReSharper disable GenericEnumeratorNotDisposed + + var ro = ReadOnlyFrugalValueListDictionaryFactory(count, out var expectedDictionary); + + // Verify struct enumerator can be obtained without using statement + _ = ro.GetEnumerator(); + + // IReadOnlyFrugalValueListDictionary enumerators + AssertEnumeratorBehavior( + count, + ro.GetEnumerator(), + expectedDictionary.GetEnumerator(), + e => e.Current); + + AssertEnumeratorBehavior( + count, + ((IReadOnlyFrugalValueListDictionary)ro).GetEnumerator(), + ((IReadOnlyFrugalValueListDictionary)expectedDictionary).GetEnumerator(), + e => e.Current); + + // IReadOnlyValueListDictionary enumerator + AssertEnumeratorBehavior( + count, + ((IReadOnlyValueListDictionary)ro).GetEnumerator(), + ((IReadOnlyValueListDictionary)expectedDictionary).GetEnumerator(), + e => e.Current); + + // IEnumerable enumerator + AssertEnumeratorBehavior( + count, + ((IEnumerable)ro).GetEnumerator(), + ((IEnumerable)expectedDictionary).GetEnumerator(), + e => e.Current); + + // ReSharper restore GenericEnumeratorNotDisposed + } + + #endregion + + #region Item Getter + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_ItemGet_DefaultKey(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out var collection); + + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary[default!]); + return; + } + + var value = CreateTValue(3452); + collection.Add(default!, value); + Assert.Equal(value, dictionary[default!].First()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_ItemGet_MissingNonDefaultKey_ThrowsKeyNotFoundException(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out _); + var missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary[missingKey]); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_ItemGet_MissingDefaultKey_ThrowsKeyNotFoundException(int count) + { + if (DefaultValueAllowed) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out var collection); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + collection.Remove(missingKey); + Assert.Throws(() => dictionary[missingKey]); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_ItemGet_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out _); + foreach (var pair in dictionary) + Assert.Equal(pair.Value, dictionary[pair.Key]); + } + + #endregion + + #region GetValues + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_GetValues_DefaultKey(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out var collection); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.GetValues(default!)); + return; + } + + var value = CreateTValue(3452); + collection.Add(default!, value); + Assert.Equal(value, dictionary.GetValues(default!).First()); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_GetValues_MissingNonDefaultKey_ThrowsKeyNotFoundException(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out _); + var missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary.GetValues(missingKey)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_GetValues_MissingDefaultKey_ThrowsKeyNotFoundException(int count) + { + if (DefaultValueAllowed) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out var collection); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + collection.Remove(missingKey); + Assert.Throws(() => dictionary.GetValues(missingKey)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_GetValues_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out _); + foreach (var pair in dictionary) + Assert.Equal(pair.Value, dictionary.GetValues(pair.Key)); + } + + #endregion + + #region TryGetValues + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_TryGetValues_DefaultKey(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out var collection); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary.TryGetValues(default!, out _)); + return; + } + + var first = CreateTValue(3452); + var second = CreateTValue(5431); + collection.Add(default!, first); + collection.Add(default!, second); + Assert.True(dictionary.TryGetValues(default!, out var valueList)); + Assert.Equal([first, second], valueList); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_TryGetValues_MissingNonDefaultKey_ReturnsFalseAndSetsDefault(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out _); + var missingKey = GetNewKey(dictionary); + Assert.False(dictionary.TryGetValues(missingKey, out var valueList)); + Assert.Equal([], valueList); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_TryGetValues_MissingDefaultKey_ReturnsFalseAndSetsDefault(int count) + { + if (DefaultValueAllowed) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out var collection); + var missingKey = default(TKey)!; + while (dictionary.ContainsKey(missingKey)) + collection.Remove(missingKey); + Assert.False(dictionary.TryGetValues(missingKey, out var valueList)); + Assert.Equal([], valueList); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ReadOnlyFrugalValueListDictionary_TryGetValues_PresentKeyReturnsCorrectValue(int count) + { + var dictionary = ReadOnlyFrugalValueListDictionaryFactory(count, out _); + foreach (var pair in dictionary) + { + Assert.True(dictionary.TryGetValues(pair.Key, out var valueList)); + Assert.Equal(pair.Value, valueList); + } + } + + #endregion + + private static void AssertEnumeratorBehavior( + int count, + TEnumerator roEnumerator, + TEnumerator expectedEnumerator, + Func getCurrent) + where TEnumerator : IEnumerator + { + try + { + for (var iteration = 0; iteration < 3; iteration++) + { + for (var j = 0; j < count; j++) + { + Assert.True(expectedEnumerator.MoveNext()); + Assert.True(roEnumerator.MoveNext()); + + var expectedCurrent = getCurrent(expectedEnumerator); + var roCurrent = getCurrent(roEnumerator); + + Assert.Equal(expectedCurrent!.GetType(), roCurrent!.GetType()); + Assert.Equal(expectedCurrent, roCurrent); + } + + Assert.False(roEnumerator.MoveNext()); + Assert.False(expectedEnumerator.MoveNext()); + + roEnumerator.Reset(); + expectedEnumerator.Reset(); + } + } + finally + { + (roEnumerator as IDisposable)?.Dispose(); + (expectedEnumerator as IDisposable)?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTests.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTests.cs similarity index 74% rename from src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTests.cs rename to src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTests.cs index f27d170..962ceea 100644 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTests.cs +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/Frugal/ReadOnlyFrugalValueListDictionaryTests.cs @@ -1,9 +1,9 @@ using System; -// ReSharper disable InconsistentNaming -namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.Frugal; -public class ReadOnlyValueListDictionaryTest_string_string : ReadOnlyValueListDictionaryTestBase +public class ReadOnlyFrugalValueListDictionaryTest_string_string + : ReadOnlyFrugalValueListDictionaryTestBase { protected override bool DefaultValueAllowed => false; @@ -22,7 +22,7 @@ protected override string CreateTValue(int seed) } } -public class ReadOnlyValueListDictionaryTest_int_int : ReadOnlyValueListDictionaryTestBase +public class ReadOnlyFrugalValueListDictionaryTest_int_int : ReadOnlyFrugalValueListDictionaryTestBase { protected override bool DefaultValueAllowed => true; @@ -36,5 +36,4 @@ protected override int CreateTValue(int seed) { return CreateTKey(seed); } -} - +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTest.Keys.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTest.Keys.cs new file mode 100644 index 0000000..e3ebb9b --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTest.Keys.cs @@ -0,0 +1,25 @@ +using System; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.IList; + +// ReSharper disable once InconsistentNaming +public class ReadOnlyValueListDictionary_Keys : ValueListDictionary_Keys_TestSuite +{ + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory() + { + return new ReadOnlyValueListDictionary(new ValueListDictionary()); + } + + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory(IValueListDictionary dictionary) + { + return new ReadOnlyValueListDictionary(dictionary); + } + + [Fact] + public void ReadOnlyValueListDictionary_KeyCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ReadOnlyValueListDictionary.KeyCollection(null!)); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTest.Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTest.Values.cs new file mode 100644 index 0000000..3439b34 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTest.Values.cs @@ -0,0 +1,27 @@ +using System; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.IList; + +// ReSharper disable once InconsistentNaming + +public class ReadOnlyValueListDictionary_Values : ValueListDictionary_Values_TestSuite +{ + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory() + { + return new ReadOnlyValueListDictionary(new ValueListDictionary()); + } + + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory(IValueListDictionary dictionary) + { + return new ReadOnlyValueListDictionary(dictionary); + } + + [Fact] + public void ReadOnlyValueListDictionary_ValueCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ReadOnlyValueListDictionary.ValueCollection(null!)); + } +} + diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTestBase.cs new file mode 100644 index 0000000..d41993a --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTestBase.cs @@ -0,0 +1,32 @@ +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using System; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.IList; + +public abstract class ReadOnlyValueListDictionaryTestBase + : ReadOnlyValueListDictionaryBaseTestSuite + where TKey : notnull +{ + protected sealed override ReadOnlyValueListDictionaryBase ReadOnlyValueListDictionaryFactory( + IReadOnlyValueListDictionary dictionary) + { + return new ReadOnlyValueListDictionary(dictionary); + } + + [Fact] + public void CtorTests_Negative() + { + AssertExtensions.Throws("dictionary", + () => _ = new ReadOnlyValueListDictionary(null!)); + } + + [Fact] + public static void Empty_Idempotent() + { + Assert.NotNull(ReadOnlyValueListDictionary.Empty); + Assert.Equal(0, ReadOnlyValueListDictionary.Empty.ValueCount); + Assert.Same(ReadOnlyValueListDictionary.Empty, ReadOnlyValueListDictionary.Empty); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTests.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTests.cs new file mode 100644 index 0000000..a226d26 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ReadOnlyValueListDictionaryTests.cs @@ -0,0 +1,63 @@ +using AnakinRaW.CommonUtilities.Collections; +using System; + +// ReSharper disable InconsistentNaming + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.IList; + +public class ReadOnlyValueListDictionaryTest_string_string : ReadOnlyValueListDictionaryTestBase +{ + protected override bool DefaultValueAllowed => false; + + protected override string CreateTKey(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + + protected override string CreateTValue(int seed) + { + return CreateTKey(seed); + } +} + +public class ReadOnlyValueListDictionaryTest_int_int : ReadOnlyValueListDictionaryTestBase +{ + protected override bool DefaultValueAllowed => true; + + protected override int CreateTKey(int seed) + { + var rand = new Random(seed); + return rand.Next(); + } + + protected override int CreateTValue(int seed) + { + return CreateTKey(seed); + } +} + +public class ReadOnlyValueListDictionaryTest_FromFrugal : ReadOnlyValueListDictionaryTestBase +{ + protected override bool DefaultValueAllowed => true; + + protected override IValueListDictionary MutableValueListDictionaryFactory() + { + return new FrugalValueListDictionary(); + } + + protected override int CreateTKey(int seed) + { + var rand = new Random(seed); + return rand.Next(); + } + + protected override int CreateTValue(int seed) + { + return CreateTKey(seed); + } +} + diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTest.Keys.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTest.Keys.cs new file mode 100644 index 0000000..3ee9a8c --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTest.Keys.cs @@ -0,0 +1,25 @@ +using System; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.IList; + +// ReSharper disable once InconsistentNaming +public class ValueListDictionary_Keys : ValueListDictionary_Keys_TestSuite +{ + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory() + { + return (ValueListDictionary)MutableValueListDictionaryFactory(); + } + + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory(IValueListDictionary dictionary) + { + return (ValueListDictionary)dictionary; + } + + [Fact] + public void ValueListDictionary_KeyCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ValueListDictionary.KeyCollection(null!)); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTest.Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTest.Values.cs new file mode 100644 index 0000000..dec0019 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTest.Values.cs @@ -0,0 +1,25 @@ +using System; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.IList; + +// ReSharper disable once InconsistentNaming +public class ValueListDictionary_Values : ValueListDictionary_Values_TestSuite +{ + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory() + { + return (ValueListDictionary)MutableValueListDictionaryFactory(); + } + + protected override IReadOnlyValueListDictionary ValueListDictionaryFactory(IValueListDictionary dictionary) + { + return (ValueListDictionary)dictionary; + } + + [Fact] + public void ValueListDictionary_ValueCollection_Constructor_NullDictionary() + { + Assert.Throws(() => new ValueListDictionary.ValueCollection(null!)); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTestBase.cs new file mode 100644 index 0000000..cb00939 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTestBase.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using AnakinRaW.CommonUtilities.Collections; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.IList; + +public abstract class ValueListDictionaryTestBase + : ValueListDictionaryBaseTestSuite> where TKey : notnull +{ + protected override ValueListDictionaryBase> + ValueListDictionaryFactory(IEqualityComparer? comparer = null) + { + return new ValueListDictionary(comparer); + } + + #region Constructors + + [Fact] + public void Ctor_InitializesCorrectly() + { + var dict = new ValueListDictionary(); + Assert.Equal(0, dict.ValueCount); + Assert.Equal(0, dict.Count); + } + + #endregion +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTests.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTests.cs similarity index 92% rename from src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTests.cs rename to src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTests.cs index a30046c..fe51cbd 100644 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTests.cs +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IList/ValueListDictionaryTests.cs @@ -1,10 +1,13 @@ using System; + // ReSharper disable InconsistentNaming -namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary.IList; public class ValueListDictionaryTest_string_string : ValueListDictionaryTestBase { + protected override bool DefaultValueAllowed => false; + protected override string CreateTKey(int seed) { var stringLength = seed % 10 + 5; @@ -23,7 +26,7 @@ protected override string CreateTValue(int seed) public class ValueListDictionaryTest_int_int : ValueListDictionaryTestBase { protected override bool DefaultValueAllowed => true; - + protected override int CreateTKey(int seed) { var rand = new Random(seed); diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs index fbe90ec..d269c05 100644 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IReadOnlyValueListDictionaryTestBase.cs @@ -9,62 +9,38 @@ namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; #pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. -public abstract class IReadOnlyValueListDictionaryTestBase : IEnumerableTestSuite>> +public abstract class IReadOnlyValueListDictionaryTestBase : IEnumerableTestSuite>> where TKey : notnull { protected abstract bool DefaultValueAllowed { get; } - - protected virtual bool IsReadOnly => true; - - protected override bool Enumerator_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; - protected override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; + // ReSharper disable once InconsistentNaming + protected virtual bool ValueList_IsReadOnlyView => true; + protected virtual bool IsReadOnly => true; - protected override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; + protected sealed override bool Enumerator_Empty_UsesSingletonInstance => true; + protected sealed override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected sealed override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected sealed override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; + protected sealed override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; protected abstract TKey CreateTKey(int seed); protected abstract TValue CreateTValue(int seed); - + protected abstract IReadOnlyValueListDictionary IReadOnlyValueListDictionaryFactory(int count); - protected override IEnumerable>> GenericIEnumerableFactory(int count) + protected sealed override KeyValuePair> CreateT(int seed) { - return IReadOnlyValueListDictionaryFactory(count); + throw new NotSupportedException(); } - - protected override IEqualityComparer>> GetIEqualityComparer() + + protected sealed override IEqualityComparer>> GetIEqualityComparer() { return new KVPComparer(); } - protected TKey GetNewKey(IReadOnlyValueListDictionary dictionary) - { - var seed = 840; - var missingKey = CreateTKey(seed++); - while (dictionary.ContainsKey(missingKey) || missingKey.Equals(default(TKey))) - missingKey = CreateTKey(seed++); - return missingKey; - } - - protected void AddToCollection(IValueListDictionary dictionary, int numberOfItemsToAdd) - { - var seed = 12353; - var random = new Random(); - var initialCount = dictionary.KeyCount; - while (dictionary.KeyCount - initialCount < numberOfItemsToAdd) - { - var toAdd = CreateTKey(seed++); - while (dictionary.ContainsKey(toAdd)) - toAdd = CreateTKey(seed++); - - dictionary.Add(toAdd, CreateTValue(seed++)); - while (random.Next() % 2 == 0) - dictionary.Add(toAdd, CreateTValue(seed++)); - } - } - - protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) + protected sealed override IEnumerable GetModifyEnumerables(ModifyOperation operations) { // ReSharper disable UseMethodAny.0 if (IsReadOnly) @@ -120,6 +96,38 @@ protected override IEnumerable GetModifyEnumerables(ModifyOper // ReSharper restore UseMethodAny.0 } + protected override IEnumerable>> GenericIEnumerableFactory( + int count) + { + return IReadOnlyValueListDictionaryFactory(count); + } + + protected void AddToCollection(IValueListDictionary dictionary, int numberOfItemsToAdd) + { + var seed = 12353; + var random = new Random(); + var initialCount = dictionary.Count; + while (dictionary.Count - initialCount < numberOfItemsToAdd) + { + var toAdd = CreateTKey(seed++); + while (dictionary.ContainsKey(toAdd)) + toAdd = CreateTKey(seed++); + + dictionary.Add(toAdd, CreateTValue(seed++)); + while (random.Next() % 2 == 0) + dictionary.Add(toAdd, CreateTValue(seed++)); + } + } + + protected TKey GetNewKey(IReadOnlyValueListDictionary dictionary) + { + var seed = 840; + var missingKey = CreateTKey(seed++); + while (dictionary.ContainsKey(missingKey) || missingKey.Equals(default(TKey))) + missingKey = CreateTKey(seed++); + return missingKey; + } + #region Item Getter [Theory] @@ -245,27 +253,27 @@ public void Values_Enumeration_Reset(int count) #endregion - #region Count + #region ValueCount [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Count_Validity(int count) + public void ValueCount_Validity(int count) { var dictionary = IReadOnlyValueListDictionaryFactory(count); var expectedCount = dictionary.Sum(pair => pair.Value.Count); - Assert.Equal(expectedCount, dictionary.Count); + Assert.Equal(expectedCount, dictionary.ValueCount); } #endregion - #region KeyCount + #region Count [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void KeyCount_Validity(int count) + public void Count_Validity(int count) { var dictionary = IReadOnlyValueListDictionaryFactory(count); - Assert.Equal(count, dictionary.KeyCount); + Assert.Equal(count, dictionary.Count); } #endregion @@ -392,6 +400,47 @@ public void GetValues_PresentKeyReturnsCorrectValue(int count) Assert.Equal(pair.Value, dictionary.GetValues(pair.Key)); } + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void GetValues_ReturnsReadOnlyViewOrSnapshot(int count) + { + if (IsReadOnly) + return; + + var dict = IReadOnlyValueListDictionaryFactory(count); + var key = GetNewKey(dict); + var seed = 1234; + AddValue(dict, key, CreateTValue(seed++)); + + var values = dict.GetValues(key); + + var newValue = CreateTValue(seed); + while (values.Contains(newValue)) + newValue = CreateTValue(++seed); + + AddValue(dict, key, newValue); + + // View reflects live changes, snapshot doesn't + Assert.Equal(ValueList_IsReadOnlyView, values.Contains(newValue)); + + // After removal, neither view nor snapshot contains the value + RemoveValue(dict, key, newValue); + Assert.DoesNotContain(newValue, values); + + // Removing key doesn't clear underlying list + RemoveKey(dict, key); + Assert.NotEmpty(values); + + // Clearing dict doesn't clear underlying lists + if (count > 0) + { + var firstKey = dict.Keys.First(); + var firstValues = dict.GetValues(firstKey); + ClearDict(dict); + Assert.NotEmpty(firstValues); + } + } + #endregion #region GetFirstValue @@ -539,7 +588,7 @@ public void TryGetValues_MissingNonDefaultKey_ReturnsFalseAndSetsDefault(int cou var dictionary = IReadOnlyValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); Assert.False(dictionary.TryGetValues(missingKey, out var valueList)); - Assert.Equal(default, valueList); + Assert.Equal([], valueList); } [Theory] @@ -553,7 +602,7 @@ public void TryGetValues_MissingDefaultKey_ReturnsFalseAndSetsDefault(int count) while (dictionary.ContainsKey(missingKey)) RemoveKey(dictionary, missingKey); Assert.False(dictionary.TryGetValues(missingKey, out var valueList)); - Assert.Equal(default, valueList); + Assert.Equal([], valueList); } } @@ -717,10 +766,28 @@ private void AddValue(IReadOnlyValueListDictionary dictionary, TKe mutable.Add(key, value); } + private void ClearDict(IReadOnlyValueListDictionary dictionary) + { + if (IsReadOnly) + throw new NotSupportedException("Test is read-only."); + if (dictionary is not IValueListDictionary mutable) + throw new InvalidOperationException("Could not cast to mutable version"); + mutable.Clear(); + } + + private void RemoveValue(IReadOnlyValueListDictionary dictionary, TKey key, TValue value) + { + if (IsReadOnly) + throw new NotSupportedException("Test is read-only."); + if (dictionary is not IValueListDictionary mutable) + throw new InvalidOperationException("Could not cast to mutable version"); + mutable.Remove(key, value); + } + // ReSharper disable once InconsistentNaming - public class KVPComparer : IEqualityComparer>> + public class KVPComparer : IEqualityComparer>> { - public bool Equals(KeyValuePair> x, KeyValuePair> y) + public bool Equals(KeyValuePair> x, KeyValuePair> y) { if (!Equals(x.Key, y.Key)) return false; @@ -730,7 +797,7 @@ public bool Equals(KeyValuePair> x, KeyValuePai return !x.Value.Where((t, i) => !Equals(t, y.Value[i])).Any(); } - public int GetHashCode(KeyValuePair> obj) + public int GetHashCode(KeyValuePair> obj) { var hashCode = new HashCode(); diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs index 790b6c7..84a1900 100644 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/IValueListDictionaryTestBase.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.EqualityComparers; using Xunit; namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; @@ -12,6 +12,9 @@ public abstract class IValueListDictionaryTestBase : IReadOnlyValu { protected override bool IsReadOnly => false; + // ReSharper disable once InconsistentNaming + protected bool Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified => true; + protected abstract IValueListDictionary IValueListDictionaryFactory(IEqualityComparer? comparer = null); protected virtual IValueListDictionary IValueListDictionaryFactory(int count) @@ -25,12 +28,12 @@ protected override IReadOnlyValueListDictionary IReadOnlyValueList { return IValueListDictionaryFactory(count); } - + #region Keys [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Keys_ModifyingTheDictionaryUpdatesTheCollection(int count) + public void IValueListDictionary_Keys_ModifyingTheDictionaryUpdatesTheCollection(int count) { var dictionary = IValueListDictionaryFactory(count); var keys = dictionary.Keys; @@ -40,18 +43,31 @@ public void Keys_ModifyingTheDictionaryUpdatesTheCollection(int count) Assert.Empty(keys); } - + [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Keys_Enumeration_ParentDictionaryModifiedInvalidates(int count) + public void IValueListDictionary_Keys_Enumeration_ParentDictionaryModifiedInvalidates(int count) { - var dictionary = IValueListDictionaryFactory(count); - var keys = dictionary.Keys; - using var keysEnum = keys.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - - Assert.Throws(() => keysEnum.MoveNext()); - Assert.Throws(() => keysEnum.Reset()); + if (!IsReadOnly) + { + var dictionary = IValueListDictionaryFactory(count); + var keys = dictionary.Keys; + using var keysEnum = keys.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (count == 0 ? Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException : Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) + { + Assert.Throws(() => keysEnum.MoveNext()); + Assert.Throws(() => keysEnum.Reset()); + } + else + { + if (keysEnum.MoveNext()) + { + _ = keysEnum.Current; + } + keysEnum.Reset(); + } + } } #endregion @@ -60,26 +76,38 @@ public void Keys_Enumeration_ParentDictionaryModifiedInvalidates(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Values_Enumeration_ParentDictionaryModifiedInvalidates(int count) + public void IValueListDictionary_Values_Enumeration_ParentDictionaryModifiedInvalidates(int count) { - var dictionary = IValueListDictionaryFactory(count); - var values = dictionary.Values; - using var valuesEnum = values.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (valuesEnum.MoveNext()) + if (!IsReadOnly) { - _ = valuesEnum.Current; + var dictionary = IValueListDictionaryFactory(count); + var values = dictionary.Values; + using var valuesEnum = values.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (count == 0 ? Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException : Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) + { + Assert.Throws(() => valuesEnum.MoveNext()); + Assert.Throws(() => valuesEnum.Reset()); + } + else + { + if (valuesEnum.MoveNext()) + { + _ = valuesEnum.Current; + } + valuesEnum.Reset(); + } } - valuesEnum.Reset(); } + [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Values_IncludeDuplicatesMultipleTimes(int count) + public void IValueListDictionary_Values_IncludeDuplicatesMultipleTimes(int count) { var dictionary = IValueListDictionaryFactory(count); - var oldValueCount = dictionary.Count; - var oldKeyCount = dictionary.KeyCount; + var oldValueCount = dictionary.ValueCount; + var oldCount = dictionary.Count; var seed = 431; foreach (var pair in dictionary.ToList()) { @@ -88,12 +116,12 @@ public void Values_IncludeDuplicatesMultipleTimes(int count) missingKey = CreateTKey(seed++); dictionary.Add(missingKey, pair.Value.First()); } - Assert.Equal(oldValueCount + oldKeyCount, dictionary.Values.Count); + Assert.Equal(oldValueCount + oldCount, dictionary.Values.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Values_ModifyingTheDictionaryUpdatesTheCollection(int count) + public void IValueListDictionary_Values_ModifyingTheDictionaryUpdatesTheCollection(int count) { var dictionary = IValueListDictionaryFactory(count); var values = dictionary.Values; @@ -110,17 +138,17 @@ public void Values_ModifyingTheDictionaryUpdatesTheCollection(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Add_DefaultKey_DefaultValue(int count) + public void IValueListDictionary_Add_DefaultKey_DefaultValue(int count) { var dictionary = IValueListDictionaryFactory(count); - var valueCoutBeforeAdd = dictionary.Count; + var valueCoutBeforeAdd = dictionary.ValueCount; var missingKey = default(TKey)!; var value = default(TValue)!; if (DefaultValueAllowed) { - Assert.False(dictionary.Add(missingKey, value)); - Assert.Equal(valueCoutBeforeAdd + 1, dictionary.Count); - Assert.Equal(count + 1, dictionary.KeyCount); + Assert.True(dictionary.Add(missingKey, value)); + Assert.Equal(valueCoutBeforeAdd + 1, dictionary.ValueCount); + Assert.Equal(count + 1, dictionary.Count); Assert.Equal(value, dictionary[missingKey].First()); Assert.Equal(value, dictionary[missingKey].Last()); } @@ -132,17 +160,17 @@ public void Add_DefaultKey_DefaultValue(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Add_DefaultKey_NonDefaultValue(int count) + public void IValueListDictionary_Add_DefaultKey_NonDefaultValue(int count) { var dictionary = IValueListDictionaryFactory(count); - var valueCoutBeforeAdd = dictionary.Count; + var valueCoutBeforeAdd = dictionary.ValueCount; var missingKey = default(TKey)!; var value = CreateTValue(1456); if (DefaultValueAllowed) { - Assert.False(dictionary.Add(missingKey, value)); - Assert.Equal(valueCoutBeforeAdd + 1, dictionary.Count); - Assert.Equal(count + 1, dictionary.KeyCount); + Assert.True(dictionary.Add(missingKey, value)); + Assert.Equal(valueCoutBeforeAdd + 1, dictionary.ValueCount); + Assert.Equal(count + 1, dictionary.Count); Assert.Equal(value, dictionary[missingKey].First()); Assert.Equal(value, dictionary[missingKey].Last()); } @@ -154,72 +182,72 @@ public void Add_DefaultKey_NonDefaultValue(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Add_NonDefaultKey_DefaultValue(int count) + public void IValueListDictionary_Add_NonDefaultKey_DefaultValue(int count) { var dictionary = IValueListDictionaryFactory(count); - var valueCoutBeforeAdd = dictionary.Count; + var valueCoutBeforeAdd = dictionary.ValueCount; var missingKey = GetNewKey(dictionary); var value = default(TValue)!; - Assert.False(dictionary.Add(missingKey, value)); - Assert.Equal(valueCoutBeforeAdd + 1, dictionary.Count); - Assert.Equal(count + 1, dictionary.KeyCount); + Assert.True(dictionary.Add(missingKey, value)); + Assert.Equal(valueCoutBeforeAdd + 1, dictionary.ValueCount); + Assert.Equal(count + 1, dictionary.Count); Assert.Equal(value, dictionary[missingKey].First()); Assert.Equal(value, dictionary[missingKey].Last()); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Add_NonDefaultKey_NonDefaultValue(int count) + public void IValueListDictionary_Add_NonDefaultKey_NonDefaultValue(int count) { var dictionary = IValueListDictionaryFactory(count); - var valueCoutBeforeAdd = dictionary.Count; + var valueCoutBeforeAdd = dictionary.ValueCount; var missingKey = GetNewKey(dictionary); var value = CreateTValue(1342); - Assert.False(dictionary.Add(missingKey, value)); - Assert.Equal(valueCoutBeforeAdd + 1, dictionary.Count); - Assert.Equal(count + 1, dictionary.KeyCount); + Assert.True(dictionary.Add(missingKey, value)); + Assert.Equal(valueCoutBeforeAdd + 1, dictionary.ValueCount); + Assert.Equal(count + 1, dictionary.Count); Assert.Equal(value, dictionary[missingKey].First()); Assert.Equal(value, dictionary[missingKey].Last()); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Add_DuplicateValue(int count) + public void IValueListDictionary_Add_DuplicateValue(int count) { var dictionary = IValueListDictionaryFactory(count); var seed = 321; var duplicate = CreateTValue(seed++); while (dictionary.Values.Contains(duplicate)) duplicate = CreateTValue(seed++); - Assert.False(dictionary.Add(GetNewKey(dictionary), duplicate)); - Assert.False(dictionary.Add(GetNewKey(dictionary), duplicate)); + Assert.True(dictionary.Add(GetNewKey(dictionary), duplicate)); + Assert.True(dictionary.Add(GetNewKey(dictionary), duplicate)); Assert.Equal(2, dictionary.Values.Count(value => value!.Equals(duplicate))); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Add_DuplicateKey_AddsToList(int count) + public void IValueListDictionary_Add_DuplicateKey_AddsToList(int count) { var dictionary = IValueListDictionaryFactory(count); var duplicateKey = GetNewKey(dictionary); - Assert.False(dictionary.Add(duplicateKey, CreateTValue(34251))); + Assert.True(dictionary.Add(duplicateKey, CreateTValue(34251))); Assert.Single(dictionary[duplicateKey]); - var valueCountBeforeSecondAdd = dictionary.Count; - var keyCountBeforeSecondAdd = dictionary.KeyCount; + var valueCountBeforeSecondAdd = dictionary.ValueCount; + var countBeforeSecondAdd = dictionary.Count; - Assert.True(dictionary.Add(duplicateKey, CreateTValue(134))); + Assert.False(dictionary.Add(duplicateKey, CreateTValue(134))); Assert.Equal(2, dictionary[duplicateKey].Count); - Assert.Equal(keyCountBeforeSecondAdd, dictionary.KeyCount); - Assert.Equal(valueCountBeforeSecondAdd + 1, dictionary.Count); + Assert.Equal(countBeforeSecondAdd, dictionary.Count); + Assert.Equal(valueCountBeforeSecondAdd + 1, dictionary.ValueCount); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Add_DistinctValuesWithHashCollisions(int count) + public void IValueListDictionary_Add_DistinctValuesWithHashCollisions(int count) { var dictionary = IValueListDictionaryFactory(new ConstantHashCodeEqualityComparer(EqualityComparer.Default)); AddToCollection(dictionary, count); - Assert.Equal(count, dictionary.KeyCount); + Assert.Equal(count, dictionary.Count); } #endregion @@ -228,7 +256,7 @@ public void Add_DistinctValuesWithHashCollisions(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Remove_EveryKey(int count) + public void IValueListDictionary_Remove_EveryKey(int count) { var dictionary = IValueListDictionaryFactory(count); Assert.All(dictionary.Keys.ToList(), key => @@ -240,28 +268,28 @@ public void Remove_EveryKey(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Remove_ValidKeyNotContainedInDictionary(int count) + public void IValueListDictionary_Remove_ValidKeyNotContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); Assert.False(dictionary.Remove(missingKey)); - Assert.Equal(count, dictionary.KeyCount); + Assert.Equal(count, dictionary.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Remove_ValidKeyContainedInDictionary(int count) + public void IValueList_Dictionary_Remove_ValidKeyContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); dictionary.Add(missingKey, CreateTValue(34251)); Assert.True(dictionary.Remove(missingKey)); - Assert.Equal(count, dictionary.KeyCount); + Assert.Equal(count, dictionary.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Remove_DefaultKeyNotContainedInDictionary(int count) + public void IValueListDictionary_Remove_DefaultKeyNotContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); if (DefaultValueAllowed) @@ -279,7 +307,7 @@ public void Remove_DefaultKeyNotContainedInDictionary(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void Remove_DefaultKeyContainedInDictionary(int count) + public void IValueListDictionary_Remove_DefaultKeyContainedInDictionary(int count) { if (DefaultValueAllowed) { @@ -296,12 +324,12 @@ public void Remove_DefaultKeyContainedInDictionary(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void RemoveKeyValue_Everything(int count) + public void IValueListDictionary_RemoveKeyValue_Everything(int count) { var dictionary = IValueListDictionaryFactory(count); Assert.All(dictionary.Keys.ToList(), key => { - foreach (var value in dictionary.GetValues(key)) + foreach (var value in dictionary.GetValues(key).ToList()) Assert.True(dictionary.Remove(key, value)); }); Assert.Empty(dictionary); @@ -309,29 +337,29 @@ public void RemoveKeyValue_Everything(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void RemoveKeyValue_ValidKeyNotContainedInDictionary(int count) + public void IValueList_Dictionary_RemoveKeyValue_ValidKeyNotContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); Assert.False(dictionary.Remove(missingKey, default!)); - Assert.Equal(count, dictionary.KeyCount); + Assert.Equal(count, dictionary.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void RemoveKeyValue_ValidKeyContainedInDictionary(int count) + public void IValueListDictionary_RemoveKeyValue_ValidKeyContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); var value = CreateTValue(34251); dictionary.Add(missingKey, value); Assert.True(dictionary.Remove(missingKey, value)); - Assert.Equal(count, dictionary.KeyCount); + Assert.Equal(count, dictionary.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void RemoveKeyValue_ValidKeyContainedInDictionary_ValueNotContained(int count) + public void IValueListDictionary_RemoveKeyValue_ValidKeyContainedInDictionary_ValueNotContained(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -344,12 +372,12 @@ public void RemoveKeyValue_ValidKeyContainedInDictionary_ValueNotContained(int c dictionary.Add(missingKey, value); Assert.False(dictionary.Remove(missingKey, missingValue)); - Assert.Equal(count + 1, dictionary.KeyCount); + Assert.Equal(count + 1, dictionary.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void RemoveKeyValue_ValidKeyContainedInDictionary_DuplicateValues(int count) + public void IValueListDictionary_RemoveKeyValue_ValidKeyContainedInDictionary_DuplicateValues(int count) { var dictionary = IValueListDictionaryFactory(count); var missingKey = GetNewKey(dictionary); @@ -360,13 +388,13 @@ public void RemoveKeyValue_ValidKeyContainedInDictionary_DuplicateValues(int cou Assert.True(dictionary.Remove(missingKey, value)); Assert.Equal([value], dictionary.GetValues(missingKey)); - Assert.Equal(count + 1, dictionary.KeyCount); + Assert.Equal(count + 1, dictionary.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void RemoveKeyValue_DefaultKeyNotContainedInDictionary(int count) + public void IValueListDictionary_RemoveKeyValue_DefaultKeyNotContainedInDictionary(int count) { var dictionary = IValueListDictionaryFactory(count); if (DefaultValueAllowed) @@ -384,7 +412,7 @@ public void RemoveKeyValue_DefaultKeyNotContainedInDictionary(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void RemoveKeyValue_DefaultKeyContainedInDictionary(int count) + public void IValueListDictionary_RemoveKeyValue_DefaultKeyContainedInDictionary(int count) { if (DefaultValueAllowed) { @@ -402,29 +430,282 @@ public void RemoveKeyValue_DefaultKeyContainedInDictionary(int count) [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void ICollection_Generic_Clear(int count) + public void IValueListDictionary_Clear(int count) { var dictionary = IValueListDictionaryFactory(count); dictionary.Clear(); + Assert.Equal(0, dictionary.ValueCount); Assert.Equal(0, dictionary.Count); - Assert.Equal(0, dictionary.KeyCount); Assert.Empty(dictionary.Keys); Assert.Empty(dictionary.Values); } [Theory] [MemberData(nameof(ValidCollectionSizes))] - public void ICollection_Generic_Clear_Repeatedly(int count) + public void IValueListDictionary_Clear_Repeatedly(int count) { var dictionary = IValueListDictionaryFactory(count); dictionary.Clear(); dictionary.Clear(); dictionary.Clear(); + Assert.Equal(0, dictionary.ValueCount); Assert.Equal(0, dictionary.Count); - Assert.Equal(0, dictionary.KeyCount); Assert.Empty(dictionary.Keys); Assert.Empty(dictionary.Values); } #endregion + + #region AddRange(TKey, IEnumerable) + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_AddRange_NullKey_ThrowsArgumentNullException(int count) + { + if (!DefaultValueAllowed) + { + var dictionary = IValueListDictionaryFactory(count); + var values = new[] { CreateTValue(1), CreateTValue(2) }; + Assert.Throws(() => dictionary.AddRange(default!, values)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_AddRange_NullValues_ThrowsArgumentNullException(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + Assert.Throws(() => dictionary.AddRange(key, null!)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_AddRange_EmptyEnumerable_DoesNotModifyDictionary(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + var valueCountBefore = dictionary.ValueCount; + var countBefore = dictionary.Count; + + Assert.False(dictionary.AddRange(key, [])); + + Assert.Equal(valueCountBefore, dictionary.ValueCount); + Assert.Equal(countBefore, dictionary.Count); + Assert.False(dictionary.ContainsKey(key)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_AddRange_NewKey_MultipleValues(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + var values = new[] { CreateTValue(1), CreateTValue(2), CreateTValue(3) }; + var valueCountBefore = dictionary.ValueCount; + + Assert.True(dictionary.AddRange(key, values)); + + Assert.Equal(valueCountBefore + 3, dictionary.ValueCount); + Assert.Equal(count + 1, dictionary.Count); + Assert.Equal(values, dictionary.GetValues(key)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_AddRange_ExistingKey_MultipleValues(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + var initialValue = CreateTValue(100); + dictionary.Add(key, initialValue); + + var valueCountBefore = dictionary.ValueCount; + var countBefore = dictionary.Count; + var values = new[] { CreateTValue(1), CreateTValue(2), CreateTValue(3) }; + + Assert.False(dictionary.AddRange(key, values)); + + Assert.Equal(valueCountBefore + 3, dictionary.ValueCount); + Assert.Equal(countBefore, dictionary.Count); + var expectedValues = new[] { initialValue }.Concat(values).ToArray(); + Assert.Equal(expectedValues, dictionary.GetValues(key)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_AddRange_DefaultKey_MultipleValues(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = default(TKey)!; + var values = new[] { CreateTValue(1), CreateTValue(2) }; + + if (DefaultValueAllowed) + { + var valueCountBefore = dictionary.ValueCount; + Assert.True(dictionary.AddRange(key, values)); + Assert.Equal(valueCountBefore + 2, dictionary.ValueCount); + Assert.Equal(values, dictionary.GetValues(key)); + } + else + { + Assert.Throws(() => dictionary.AddRange(key, values)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_AddRange_SingleValue(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + var values = new[] { CreateTValue(1) }; + var valueCountBefore = dictionary.ValueCount; + + Assert.True(dictionary.AddRange(key, values)); + + Assert.Equal(valueCountBefore + 1, dictionary.ValueCount); + Assert.Equal(count + 1, dictionary.Count); + Assert.Equal(values, dictionary.GetValues(key)); + } + + #endregion + + #region RemoveAll(TKey, Predicate) + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_RemoveAll_NullKey_ThrowsArgumentNullException(int count) + { + if (!DefaultValueAllowed) + { + var dictionary = IValueListDictionaryFactory(count); + Assert.Throws(() => dictionary.RemoveAll(default!, _ => true)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_RemoveAll_NullPredicate_ThrowsArgumentNullException(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + Assert.Throws(() => dictionary.RemoveAll(key, null!)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_RemoveAll_KeyNotInDictionary_ReturnsZero(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + var removed = dictionary.RemoveAll(key, _ => true); + Assert.Equal(0, removed); + Assert.Equal(count, dictionary.Count); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_RemoveAll_RemoveSomeValues(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + dictionary.Add(key, CreateTValue(1)); + dictionary.Add(key, CreateTValue(2)); + dictionary.Add(key, CreateTValue(3)); + dictionary.Add(key, CreateTValue(4)); + + var valueCountBefore = dictionary.ValueCount; + var countBefore = dictionary.Count; + + var removed = dictionary.RemoveAll(key, v => v!.Equals(CreateTValue(2)) || v.Equals(CreateTValue(4))); + + Assert.Equal(2, removed); + Assert.Equal(valueCountBefore - 2, dictionary.ValueCount); + Assert.Equal(countBefore, dictionary.Count); + Assert.Equal(2, dictionary.GetValues(key).Count); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_RemoveAll_RemoveAllValues_RemovesKey(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + dictionary.Add(key, CreateTValue(1)); + dictionary.Add(key, CreateTValue(2)); + dictionary.Add(key, CreateTValue(3)); + + var valueCountBefore = dictionary.ValueCount; + var countBefore = dictionary.Count; + + var removed = dictionary.RemoveAll(key, _ => true); + + Assert.Equal(3, removed); + Assert.Equal(valueCountBefore - 3, dictionary.ValueCount); + Assert.Equal(countBefore - 1, dictionary.Count); + Assert.False(dictionary.ContainsKey(key)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_RemoveAll_PredicateMatchesNothing_ReturnsZero(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + dictionary.Add(key, CreateTValue(1)); + dictionary.Add(key, CreateTValue(2)); + + var valueCountBefore = dictionary.ValueCount; + var countBefore = dictionary.Count; + + var removed = dictionary.RemoveAll(key, _ => false); + + Assert.Equal(0, removed); + Assert.Equal(valueCountBefore, dictionary.ValueCount); + Assert.Equal(countBefore, dictionary.Count); + Assert.Equal(2, dictionary.GetValues(key).Count); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_RemoveAll_DefaultKey(int count) + { + if (DefaultValueAllowed) + { + var dictionary = IValueListDictionaryFactory(count); + var key = default(TKey)!; + dictionary.Add(key, CreateTValue(1)); + dictionary.Add(key, CreateTValue(2)); + + var removed = dictionary.RemoveAll(key, v => v!.Equals(CreateTValue(1))); + + Assert.Equal(1, removed); + Assert.Single(dictionary.GetValues(key)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IValueListDictionary_RemoveAll_RemovesOnlyMatchingValues(int count) + { + var dictionary = IValueListDictionaryFactory(count); + var key = GetNewKey(dictionary); + var value1 = CreateTValue(1); + var value2 = CreateTValue(2); + var value3 = CreateTValue(3); + + dictionary.Add(key, value1); + dictionary.Add(key, value2); + dictionary.Add(key, value3); + dictionary.Add(key, value1); + + var removed = dictionary.RemoveAll(key, v => v!.Equals(value1)); + + Assert.Equal(2, removed); + Assert.Equal(2, dictionary.GetValues(key).Count); + Assert.All(dictionary.GetValues(key), v => Assert.NotEqual(value1, v)); + } + + #endregion } \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryBaseTestSuite.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryBaseTestSuite.cs new file mode 100644 index 0000000..981d598 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryBaseTestSuite.cs @@ -0,0 +1,180 @@ +using System; +using AnakinRaW.CommonUtilities.Collections; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +public abstract class ReadOnlyValueListDictionaryBaseTestSuite : IReadOnlyValueListDictionaryTestBase + where TKey : notnull +{ + protected override bool DefaultValueAllowed => false; + + protected override bool IsReadOnly => true; + + protected virtual IValueListDictionary MutableValueListDictionaryFactory() + { + return new ValueListDictionary(); + } + + protected abstract ReadOnlyValueListDictionaryBase ReadOnlyValueListDictionaryFactory( + IReadOnlyValueListDictionary dictionary); + + protected ReadOnlyValueListDictionaryBase ReadOnlyValueListDictionaryFactory(int count) + { + var collection = MutableValueListDictionaryFactory(); + AddToCollection(collection, count); + return ReadOnlyValueListDictionaryFactory(collection); + } + + protected sealed override IReadOnlyValueListDictionary IReadOnlyValueListDictionaryFactory(int count) + { + return ReadOnlyValueListDictionaryFactory(count); + } + + protected override IEnumerable>> GenericIEnumerableFactory( + int count) + { + var collection = MutableValueListDictionaryFactory(); + AddToCollection(collection, count); + return ReadOnlyValueListDictionaryFactory(collection); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void CtorTests(int count) + { + var collection = MutableValueListDictionaryFactory(); + AddToCollection(collection, count); + var readOnlyDictionary = ReadOnlyValueListDictionaryFactory(collection); + + Assert.Equal(collection.Count, readOnlyDictionary.Count); + Assert.Equal(collection.ValueCount, readOnlyDictionary.ValueCount); + + VerifyReadOnlyValueListDictionary(readOnlyDictionary, collection); + VerifyReadOnlyValueListDictionary(ReadOnlyValueListDictionaryFactory(readOnlyDictionary), collection); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void SourceModificationsReflectedInReadOnlyDictionary(int count) + { + var collection = MutableValueListDictionaryFactory(); + AddToCollection(collection, count); + var readOnlyDictionary = ReadOnlyValueListDictionaryFactory(collection); + + Assert.Equal(collection.Count, readOnlyDictionary.Count); + Assert.Equal(collection.ValueCount, readOnlyDictionary.ValueCount); + + collection.Add(GetNewKey(collection), CreateTValue(4231)); + + Assert.Equal(collection.Count, readOnlyDictionary.Count); + Assert.Equal(collection.ValueCount, readOnlyDictionary.ValueCount); + } + + private static void VerifyReadOnlyValueListDictionary( + ReadOnlyValueListDictionaryBase readOnlyDictionary, + IValueListDictionary expectedDict) + { + Assert.Equal(expectedDict.ValueCount, readOnlyDictionary.ValueCount); + foreach (var key in expectedDict.Keys) + { + var expectedValue = expectedDict[key]; + Assert.Equal(expectedValue, readOnlyDictionary[key]); + } + VerifyGenericEnumerator(readOnlyDictionary, expectedDict); + + VerifyEnumerator(readOnlyDictionary, expectedDict); + } + + private static void VerifyGenericEnumerator( + ReadOnlyValueListDictionaryBase readOnlyDictionary, + IValueListDictionary expectedDict) + { + var enumerator = readOnlyDictionary.GetEnumerator(); + var iterations = 0; + var expectedCount = expectedDict.Count; + + var keys = expectedDict.Keys.ToList(); + + while (iterations < expectedCount && enumerator.MoveNext()) + { + var currentItem = enumerator.Current; + + // Verify we have not gotten more items then we expected + Assert.True(iterations < expectedCount, + "More items have been returned from the enumerator(" + iterations + " items) " + + "then are in the expectedElements(" + expectedCount + " items)"); + + var expectedKey = keys[iterations]; + + Assert.Equal(expectedKey, currentItem.Key); + Assert.Equal(expectedDict[expectedKey], currentItem.Value); + + // Verify Current always returns the same value every time it is called + for (var i = 0; i < 3; i++) + { + var tempItem = enumerator.Current; + Assert.Equal(currentItem, tempItem); + } + + iterations++; + } + + Assert.Equal(expectedCount, iterations); + + for (var i = 0; i < 3; i++) + { + Assert.False(enumerator.MoveNext(), + "Expected MoveNext to return false after" + iterations + " iterations"); + } + + enumerator.Dispose(); + } + + private static void VerifyEnumerator( + ReadOnlyValueListDictionaryBase readOnlyDictionary, + IValueListDictionary expectedDict) + { + IEnumerator enumerator = readOnlyDictionary.GetEnumerator(); + var iterations = 0; + var expectedCount = expectedDict.Count; + + var keys = expectedDict.Keys.ToList(); + + while ((iterations < expectedCount) && enumerator.MoveNext()) + { + var currentItem = (KeyValuePair>) enumerator.Current; + + // Verify we have not gotten more items then we expected + Assert.True(iterations < expectedCount, + "More items have been returned from the enumerator(" + iterations + " items) then are in the expectedElements(" + expectedCount + " items)"); + + var expectedKey = keys[iterations]; + + Assert.Equal(expectedKey, currentItem.Key); + Assert.Equal(expectedDict[expectedKey], currentItem.Value); + + // Verify Current always returns the same value every time it is called + for (var i = 0; i < 3; i++) + { + var tempItem = enumerator.Current; + Assert.Equal(currentItem, tempItem); + } + + iterations++; + } + + Assert.Equal(expectedCount, iterations); + + for (var i = 0; i < 3; i++) + { + Assert.False(enumerator.MoveNext(), "Expected MoveNext to return false after" + iterations + " iterations"); + } + + if (enumerator is IDisposable disposable) + disposable.Dispose(); + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Keys.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Keys.cs deleted file mode 100644 index a9db5a9..0000000 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Keys.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing.Collections; -using Xunit; - -namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; - -// ReSharper disable once InconsistentNaming -public class ReadOnlyValueListDictionary_Keys : ICollectionTestSuite -{ - protected override bool Enumerator_Empty_UsesSingletonInstance => false; - protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => false; - protected override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; - protected override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; - protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; - protected override bool DefaultValueAllowed => false; - protected override bool DuplicateValuesAllowed => false; - protected override bool IsReadOnly => true; - protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); - - protected override ICollection GenericICollectionFactory() - { - return new ReadOnlyValueListDictionary(new ValueListDictionary()).Keys; - } - - protected override ICollection GenericICollectionFactory(int count) - { - var list = new ValueListDictionary(); - var seed = 13453; - var random = new Random(); - for (var i = 0; i < count; i++) - { - var key = CreateT(seed++); - list.Add(key, CreateT(seed++)); - while (random.Next() % 2 == 0) - list.Add(key, CreateT(seed++)); - } - return new ReadOnlyValueListDictionary(list).Keys; - } - - protected override string CreateT(int seed) - { - var stringLength = seed % 10 + 5; - var rand = new Random(seed); - var bytes = new byte[stringLength]; - rand.NextBytes(bytes); - return Convert.ToBase64String(bytes); - } - - [Fact] - public void ValueListDictionary_KeyCollection_Constructor_NullDictionary() - { - Assert.Throws(() => new ReadOnlyValueListDictionary.KeyCollection(null!)); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void ValueListDictionary_KeyCollection_GetEnumerator(int count) - { - var dictionary = new ValueListDictionary(); - var seed = 13453; - while (dictionary.Count < count) - dictionary.Add(CreateT(seed++), CreateT(seed++)); - using var _ = new ReadOnlyValueListDictionary(dictionary).Keys.GetEnumerator(); - } -} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Values.cs deleted file mode 100644 index 6032610..0000000 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTest.Values.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing.Collections; -using Xunit; - -namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; - -// ReSharper disable once InconsistentNaming -public class ReadOnlyValueListDictionary_Values : ICollectionTestSuite -{ - protected override bool DefaultValueAllowed => true; - protected override bool DuplicateValuesAllowed => true; - protected override bool IsReadOnly => true; - protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); - protected override bool Enumerator_Empty_UsesSingletonInstance => false; - protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; - protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => false; - - protected override ICollection GenericICollectionFactory() - { - return new ReadOnlyValueListDictionary(new ValueListDictionary()).Values; - } - - protected override ICollection GenericICollectionFactory(int count) - { - var list = new ValueListDictionary(); - var seed = 13453; - var random = new Random(seed); - - var valuesAdded = 0; - while (valuesAdded < count) - { - var key = CreateT(seed++); - - // Add first value for this key - list.Add(key, CreateT(seed++)); - valuesAdded++; - - // Randomly add more values for the same key, but don't exceed count - while (valuesAdded < count && random.Next() % 2 == 0) - { - list.Add(key, CreateT(seed++)); - valuesAdded++; - } - } - - return new ReadOnlyValueListDictionary(list).Values; - } - - protected override string CreateT(int seed) - { - var stringLength = seed % 10 + 5; - var rand = new Random(seed); - var bytes = new byte[stringLength]; - rand.NextBytes(bytes); - return Convert.ToBase64String(bytes); - } - - [Fact] - public void ValueListDictionary_ValueCollection_Constructor_NullDictionary() - { - Assert.Throws(() => new ReadOnlyValueListDictionary.ValueCollection(null!)); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void ValueListDictionary_ValueCollection_GetEnumerator(int count) - { - var dictionary = new ValueListDictionary(); - var seed = 13453; - while (dictionary.Count < count) - dictionary.Add(CreateT(seed++), CreateT(seed++)); - using var _ = new ReadOnlyValueListDictionary(dictionary).Values.GetEnumerator(); - } -} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs deleted file mode 100644 index 8a8cf7a..0000000 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ReadOnlyValueListDictionaryTestBase.cs +++ /dev/null @@ -1,94 +0,0 @@ -using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing.Extensions; -using System; -using System.Collections.Generic; -using Xunit; - -namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; - -public abstract class ReadOnlyValueListDictionaryTestBase : IReadOnlyValueListDictionaryTestBase - where TKey : notnull -{ - protected override bool DefaultValueAllowed => false; - - protected override bool IsReadOnly => true; - - protected override KeyValuePair> CreateT(int seed) - { - throw new NotSupportedException(); - } - - protected virtual ReadOnlyValueListDictionary ReadOnlyValueListDictionaryFactory(int count) - { - var collection = new ValueListDictionary(); - AddToCollection(collection, count); - return new ReadOnlyValueListDictionary(collection); - } - - protected override IReadOnlyValueListDictionary IReadOnlyValueListDictionaryFactory(int count) - { - return ReadOnlyValueListDictionaryFactory(count); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void CtorTests(int count) - { - var collection = new ValueListDictionary(); - AddToCollection(collection, count); - var readOnlyDictionary = new ReadOnlyValueListDictionary(collection); - - Assert.Equal(collection.KeyCount, readOnlyDictionary.KeyCount); - Assert.Equal(collection.Count, readOnlyDictionary.Count); - } - - [Fact] - public static void CtorTests_Negative() - { - AssertExtensions.Throws("dictionary", () => _ = new ReadOnlyValueListDictionary(null!)); - } - - [Fact] - public static void Empty_Idempotent() - { - Assert.NotNull(ReadOnlyValueListDictionary.Empty); - Assert.Equal(0, ReadOnlyValueListDictionary.Empty.Count); - Assert.Same(ReadOnlyValueListDictionary.Empty, ReadOnlyValueListDictionary.Empty); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void EnumeratorTest(int count) - { - var collection = new ValueListDictionary(); - AddToCollection(collection, count); - var readOnlyDictionary = new ReadOnlyValueListDictionary(collection); - - using var enumerator = readOnlyDictionary.GetEnumerator(); - foreach (var keyValuePair in collection) - { - Assert.True(enumerator.MoveNext()); - - Assert.Equal(keyValuePair.Key, enumerator.Current.Key); - Assert.Equal(keyValuePair.Value, enumerator.Current.Value); - } - Assert.False(enumerator.MoveNext()); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void SourceModificationsReflectedInReadOnlyDictionary(int count) - { - var collection = new ValueListDictionary(); - AddToCollection(collection, count); - var readOnlyDictionary = new ReadOnlyValueListDictionary(collection); - - Assert.Equal(collection.KeyCount, readOnlyDictionary.KeyCount); - Assert.Equal(collection.Count, readOnlyDictionary.Count); - - collection.Add(GetNewKey(collection), CreateTValue(4231)); - - Assert.Equal(collection.KeyCount, readOnlyDictionary.KeyCount); - Assert.Equal(collection.Count, readOnlyDictionary.Count); - } -} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestBase.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryBaseTestSuite.cs similarity index 55% rename from src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestBase.cs rename to src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryBaseTestSuite.cs index 8f6c02a..f16cc53 100644 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestBase.cs +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryBaseTestSuite.cs @@ -1,20 +1,15 @@ -using System; +using AnakinRaW.CommonUtilities.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; -using AnakinRaW.CommonUtilities.Collections; using Xunit; namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; -public abstract class ValueListDictionaryTestBase : IValueListDictionaryTestBase where TKey : notnull -{ - protected override bool DefaultValueAllowed => false; - - protected override KeyValuePair> CreateT(int seed) - { - throw new NotSupportedException(); - } - +public abstract class ValueListDictionaryBaseTestSuite : IValueListDictionaryTestBase + where TKey : notnull + where TList : IList +{ protected override IValueListDictionary IValueListDictionaryFactory(IEqualityComparer? comparer = null) { return ValueListDictionaryFactory(comparer); @@ -25,26 +20,49 @@ protected override IValueListDictionary IValueListDictionaryFactor return ValueListDictionaryFactory(count); } - protected ValueListDictionary ValueListDictionaryFactory(IEqualityComparer? comparer = null) - { - return new ValueListDictionary(comparer); - } + protected abstract ValueListDictionaryBase ValueListDictionaryFactory( + IEqualityComparer? comparer = null); - protected virtual ValueListDictionary ValueListDictionaryFactory(int count) + protected virtual ValueListDictionaryBase ValueListDictionaryFactory(int count) { var collection = ValueListDictionaryFactory(); AddToCollection(collection, count); return collection; } - #region Constructors + #region Clear [Fact] - public void Dictionary_CapacityAtLeastPassedValue() + public void Clear_OnEmptyCollection_DoesNotInvalidateEnumerator() { - var dict = new ValueListDictionary(); - Assert.Equal(0, dict.Count); - Assert.Equal(0, dict.KeyCount); + if (ModifyEnumeratorAllowed.HasFlag(ModifyOperation.Clear)) + { + var dictionary = IValueListDictionaryFactory(0); + IEnumerator valuesEnum = dictionary.GetEnumerator(); + + dictionary.Clear(); + Assert.Empty(dictionary); + Assert.False(valuesEnum.MoveNext()); + } + } + + #endregion + + #region Add + + [Fact] + public void Add_ItemAlreadyExists_DoesNotInvalidateEnumerator() + { + var dictionary = IValueListDictionaryFactory(0); + var key = CreateTKey(123); + var value = CreateTValue(123); + + dictionary.Add(key, value); + + IEnumerator valuesEnum = dictionary.GetEnumerator(); + dictionary.Add(key, value); + + Assert.True(valuesEnum.MoveNext()); } #endregion diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Keys.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Keys.cs deleted file mode 100644 index 08dc4e4..0000000 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Keys.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing.Collections; -using Xunit; - -namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; - -// ReSharper disable once InconsistentNaming -public class ValueListDictionary_Keys : ICollectionTestSuite -{ - protected override bool Enumerator_Empty_UsesSingletonInstance => false; - protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => false; - protected override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; - protected override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; - protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; - protected override bool DefaultValueAllowed => false; - protected override bool DuplicateValuesAllowed => false; - protected override bool IsReadOnly => true; - protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); - - protected override ICollection GenericICollectionFactory() - { - return new ValueListDictionary().Keys; - } - - protected override ICollection GenericICollectionFactory(int count) - { - var list = new ValueListDictionary(); - var seed = 13453; - var random = new Random(); - for (var i = 0; i < count; i++) - { - var key = CreateT(seed++); - list.Add(key, CreateT(seed++)); - while (random.Next() % 2 == 0) - list.Add(key, CreateT(seed++)); - } - return list.Keys; - } - - protected override string CreateT(int seed) - { - var stringLength = seed % 10 + 5; - var rand = new Random(seed); - var bytes = new byte[stringLength]; - rand.NextBytes(bytes); - return Convert.ToBase64String(bytes); - } - - [Fact] - public void ValueListDictionary_KeyCollection_Constructor_NullDictionary() - { - Assert.Throws(() => new ValueListDictionary.KeyCollection(null!)); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void ValueListDictionary_KeyCollection_GetEnumerator(int count) - { - var dictionary = new ValueListDictionary(); - var seed = 13453; - while (dictionary.Count < count) - dictionary.Add(CreateT(seed++), CreateT(seed++)); - using var _ = dictionary.Keys.GetEnumerator(); - } -} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Values.cs deleted file mode 100644 index ee9cf0d..0000000 --- a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTest.Values.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using AnakinRaW.CommonUtilities.Collections; -using AnakinRaW.CommonUtilities.Testing.Collections; -using Xunit; - -namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; - -// ReSharper disable once InconsistentNaming -public class ValueListDictionary_Values : ICollectionTestSuite -{ - protected override bool DefaultValueAllowed => true; - protected override bool DuplicateValuesAllowed => true; - protected override bool IsReadOnly => true; - protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); - protected override bool Enumerator_Empty_UsesSingletonInstance => false; - protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; - protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => false; - - protected override ICollection GenericICollectionFactory() - { - return new ValueListDictionary().Values; - } - - protected override ICollection GenericICollectionFactory(int count) - { - var list = new ValueListDictionary(); - var seed = 13453; - var random = new Random(seed); - - var valuesAdded = 0; - while (valuesAdded < count) - { - var key = CreateT(seed++); - - // Add first value for this key - list.Add(key, CreateT(seed++)); - valuesAdded++; - - // Randomly add more values for the same key, but don't exceed count - while (valuesAdded < count && random.Next() % 2 == 0) - { - list.Add(key, CreateT(seed++)); - valuesAdded++; - } - } - - return list.Values; - } - - protected override string CreateT(int seed) - { - var stringLength = seed % 10 + 5; - var rand = new Random(seed); - var bytes = new byte[stringLength]; - rand.NextBytes(bytes); - return Convert.ToBase64String(bytes); - } - - [Fact] - public void ValueListDictionary_ValueCollection_Constructor_NullDictionary() - { - Assert.Throws(() => new ValueListDictionary.ValueCollection(null!)); - } - - [Theory] - [MemberData(nameof(ValidCollectionSizes))] - public void ValueListDictionary_ValueCollection_GetEnumerator(int count) - { - var dictionary = new ValueListDictionary(); - var seed = 13453; - while (dictionary.Count < count) - dictionary.Add(CreateT(seed++), CreateT(seed++)); - using var _ = dictionary.Values.GetEnumerator(); - } -} \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestSuite.Keys_Values.cs b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestSuite.Keys_Values.cs new file mode 100644 index 0000000..53d4602 --- /dev/null +++ b/src/CommonUtilities/test/Collections/ValueListDictionary/ValueListDictionaryTestSuite.Keys_Values.cs @@ -0,0 +1,137 @@ +using AnakinRaW.CommonUtilities.Collections; +using AnakinRaW.CommonUtilities.Testing.Collections; +using System; +using System.Collections.Generic; +using Xunit; + +namespace AnakinRaW.CommonUtilities.Test.Collections.ValueListDictionary; + +public abstract class ValueListDictionary_Keys_Values_CollectionTestSuite : ICollectionTestSuite +{ + public enum CollectionSelector + { + Keys, + Values + } + + protected abstract CollectionSelector Selector { get; } + + protected sealed override bool IsReadOnly => true; + protected sealed override bool Enumerator_Empty_UsesSingletonInstance => true; + protected sealed override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected sealed override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected sealed override bool NonGenericEnumerator_Empty_Current_UndefinedOperation_Throw => true; + protected sealed override bool NonGenericEnumerator_Current_UndefinedOperation_Throws => true; + + protected sealed override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + + protected sealed override ICollection GenericICollectionFactory() + { + var dictionary = ValueListDictionaryFactory(); + return Selector is CollectionSelector.Keys + ? dictionary.Keys + : dictionary.Values; + } + + protected sealed override ICollection GenericICollectionFactory(int count) + { + var mutableDictionary = MutableValueListDictionaryFactory(); + Populate(mutableDictionary, count); + + var dictionary = ValueListDictionaryFactory(mutableDictionary); + return Selector is CollectionSelector.Keys + ? dictionary.Keys + : dictionary.Values; + } + + protected sealed override string CreateT(int seed) + { + var stringLength = seed % 10 + 5; + var rand = new Random(seed); + var bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + protected abstract IReadOnlyValueListDictionary ValueListDictionaryFactory(); + + protected abstract IReadOnlyValueListDictionary ValueListDictionaryFactory( + IValueListDictionary dictionary); + + protected abstract void Populate(IValueListDictionary dictionary, int count); + + protected virtual IValueListDictionary MutableValueListDictionaryFactory() + { + return new ValueListDictionary(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ValueListDictionary_KeyOrValueCollection_GetEnumerator(int count) + { + var mutable = MutableValueListDictionaryFactory(); + var seed = 13453; + while (mutable.ValueCount < count) + mutable.Add(CreateT(seed++), CreateT(seed++)); + var dictionary = ValueListDictionaryFactory(mutable); + if (Selector is CollectionSelector.Keys) + { + using var _ = dictionary.Keys.GetEnumerator(); + } + else + { + using var _ = dictionary.Values.GetEnumerator(); + } + } +} + +public abstract class ValueListDictionary_Keys_TestSuite : ValueListDictionary_Keys_Values_CollectionTestSuite +{ + protected sealed override CollectionSelector Selector => CollectionSelector.Keys; + protected sealed override bool DefaultValueAllowed => false; + protected sealed override bool DuplicateValuesAllowed => false; + + protected sealed override void Populate(IValueListDictionary dictionary, int count) + { + var seed = 13453; + var random = new Random(); + for (var i = 0; i < count; i++) + { + var key = CreateT(seed++); + dictionary.Add(key, CreateT(seed++)); + while (random.Next() % 2 == 0) + dictionary.Add(key, CreateT(seed++)); + } + } +} + +public abstract class ValueListDictionary_Values_TestSuite : ValueListDictionary_Keys_Values_CollectionTestSuite +{ + protected sealed override CollectionSelector Selector => CollectionSelector.Values; + + protected sealed override bool DefaultValueAllowed => true; + protected sealed override bool DuplicateValuesAllowed => true; + + protected override void Populate(IValueListDictionary dictionary, int count) + { + var seed = 13453; + var random = new Random(seed); + + var valuesAdded = 0; + while (valuesAdded < count) + { + var key = CreateT(seed++); + + // Add first value for this key + dictionary.Add(key, CreateT(seed++)); + valuesAdded++; + + // Randomly add more values for the same key, but don't exceed count + while (valuesAdded < count && random.Next() % 2 == 0) + { + dictionary.Add(key, CreateT(seed++)); + valuesAdded++; + } + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities/test/CommonUtilities.Test.csproj b/src/CommonUtilities/test/CommonUtilities.Test.csproj index 1005529..c8ad3ad 100644 --- a/src/CommonUtilities/test/CommonUtilities.Test.csproj +++ b/src/CommonUtilities/test/CommonUtilities.Test.csproj @@ -15,9 +15,9 @@ - + - + @@ -41,4 +41,8 @@ + + + + \ No newline at end of file diff --git a/src/CommonUtilities/test/Hashing/HashingServiceTest.cs b/src/CommonUtilities/test/Hashing/HashingServiceTest.cs index 864106d..f28a631 100644 --- a/src/CommonUtilities/test/Hashing/HashingServiceTest.cs +++ b/src/CommonUtilities/test/Hashing/HashingServiceTest.cs @@ -10,7 +10,7 @@ using Xunit; using System.Collections.Generic; using System.Globalization; -#if NET8_0_OR_GREATER +#if NET10_0_OR_GREATER using System.Security.Cryptography; #endif @@ -258,7 +258,7 @@ public async Task GetHashAsync_AlwaysOneProvider() [MemberData(nameof(ProviderHashTestData_SHA256))] [MemberData(nameof(ProviderHashTestData_SHA384))] [MemberData(nameof(ProviderHashTestData_SHA512))] -#if NET8_0_OR_GREATER +#if NET10_0_OR_GREATER [MemberData(nameof(ProviderHashTestData_SHA3_256))] [MemberData(nameof(ProviderHashTestData_SHA3_384))] [MemberData(nameof(ProviderHashTestData_SHA3_512))] @@ -308,7 +308,7 @@ public void GetHash_DefaultProviders(HashTypeKey hashType, string input, string [MemberData(nameof(ProviderHashTestData_SHA256))] [MemberData(nameof(ProviderHashTestData_SHA384))] [MemberData(nameof(ProviderHashTestData_SHA512))] -#if NET8_0_OR_GREATER +#if NET10_0_OR_GREATER [MemberData(nameof(ProviderHashTestData_SHA3_256))] [MemberData(nameof(ProviderHashTestData_SHA3_384))] [MemberData(nameof(ProviderHashTestData_SHA3_512))] @@ -387,7 +387,7 @@ public static IEnumerable ProviderKnownHashTypes() yield return [HashTypeKey.SHA256]; yield return [HashTypeKey.SHA384]; yield return [HashTypeKey.SHA512]; -#if NET8_0_OR_GREATER +#if NET10_0_OR_GREATER if (SHA3_256.IsSupported) yield return [HashTypeKey.SHA3_256]; if (SHA3_384.IsSupported) @@ -397,7 +397,7 @@ public static IEnumerable ProviderKnownHashTypes() #endif } -#if NET8_0_OR_GREATER +#if NET10_0_OR_GREATER public static IEnumerable ProviderHashTestData_SHA3_256() { From 187b41a7e249d991441b73d6d6d598d8a657dd23 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 16 Jan 2026 11:04:34 +0100 Subject: [PATCH 33/43] make weekly deps check --- .github/dependabot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b43ddf6..8c2061c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" groups: actions-deps: patterns: @@ -19,7 +19,7 @@ updates: - package-ecosystem: "nuget" directory: "/" schedule: - interval: "daily" + interval: "weekly" target-branch: "develop" open-pull-requests-limit: 1 groups: From f6665cae6bccb237949bb872a9717e5a5e46fe7a Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 16 Jan 2026 11:04:50 +0100 Subject: [PATCH 34/43] update copyright year --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index f310960..7fe4933 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,7 +14,7 @@ AnakinRaW Common Utilities - Copyright © AnakinRaW 2025 + Copyright © AnakinRaW 2026 AnakinRaW AnakinRaW https://github.com/AnakinRaW/CommonUtilities From 67b89ecfa368b8e15d989caec349676cf70dba43 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 16 Jan 2026 11:04:57 +0100 Subject: [PATCH 35/43] update version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index b5da456..1d8d59f 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "12.3", + "version": "13.0", "assemblyVersion": { "precision": "major" }, From ab9cb8db0858ef7db1e38bc4176857b66e094ba7 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 16 Jan 2026 11:44:30 +0100 Subject: [PATCH 36/43] update --- Directory.Build.props | 3 +-- LICENSE | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 7fe4933..b80be57 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,8 +23,7 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/LICENSE b/LICENSE index e37e0b3..cdb952d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 AnakinRaW +Copyright (c) 2026 AnakinRaW Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 989307af4057f38f4f2733aaf1b1639789553495 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 16 Jan 2026 14:25:43 +0100 Subject: [PATCH 37/43] expose API to create an ImmutableFrugalList from a single value --- .../src/Collections/ImmutableFrugalList.cs | 93 +++++++++++++++---- .../FrugalList/ImmutableFrugalListTestBase.cs | 39 -------- .../FrugalList/ImmutableFrugalListTests.cs | 47 ++++++++++ 3 files changed, 124 insertions(+), 55 deletions(-) diff --git a/src/CommonUtilities/src/Collections/ImmutableFrugalList.cs b/src/CommonUtilities/src/Collections/ImmutableFrugalList.cs index 123791b..44a4d3a 100644 --- a/src/CommonUtilities/src/Collections/ImmutableFrugalList.cs +++ b/src/CommonUtilities/src/Collections/ImmutableFrugalList.cs @@ -6,7 +6,7 @@ namespace AnakinRaW.CommonUtilities.Collections; /// -/// A read-only variant of the . +/// Provides an immutable variant of the . /// /// The type of elements in the list. [DebuggerTypeProxy(typeof(IReadOnlyCollectionDebugView<>))] @@ -20,14 +20,14 @@ namespace AnakinRaW.CommonUtilities.Collections; private readonly FrugalList _list; - /// + /// public int Count => _list.Count; - /// + /// public T this[int index] => _list[index]; /// - /// Initializes a new instance of the structure to one item. + /// Initializes a new instance of the structure with the specified item. /// /// The item of the list. internal ImmutableFrugalList(T item) @@ -55,22 +55,72 @@ public void CopyTo(T[] array, int index) #region Explixit IList/ICollection implementations + /// + /// Gets the element at the specified index. An occurs if you try to set the item at the specified index. + /// + /// + /// Because the collection is immutable, you can only get this item at the specified index. + /// An exception will occur if you try to set the item. This member is an explicit interface member implementation. + /// It can be used only when the instance is cast to an interface. + /// + /// The zero-based index of the element to get. + /// The element at the specified index. + /// The element at the specified index. T IList.this[int index] { get => _list[index]; set => throw new NotSupportedException("Collection is read-only."); } + /// + /// Gets a value indicating whether the is read-only. + /// + /// + /// if the is read-only; otherwise, . + /// In the implementation of , this property always returns . + /// bool ICollection.IsReadOnly => true; + /// + /// Adds an item to the . This implementation always throws . + /// + /// The object to add to the . + /// Always thrown. void ICollection.Add(T item) => throw new NotSupportedException(); + /// + /// Inserts an item to the at the specified index. + /// This implementation always throws . + /// + /// The zero-based index at which the item should be inserted. + /// The object to insert into the . + /// Always thrown. void IList.Insert(int index, T item) => throw new NotSupportedException("Collection is read-only."); + /// + /// Removes the first occurrence of a specific object from the . + /// This implementation always throws . + /// + /// The object to remove from the list. + /// + /// Always throws a because the collection is read-only. + /// + /// Always thrown. bool ICollection.Remove(T item) => throw new NotSupportedException("Collection is read-only."); + /// + /// Removes the item at the specified index. + /// This implementation always throws . + /// + /// The zero-based index of the element to remove. + /// Always thrown. void IList.RemoveAt(int index) => throw new NotSupportedException("Collection is read-only."); + /// + /// Removes all items from the . + /// This implementation always throws . + /// + /// Always thrown. void ICollection.Clear() => throw new NotSupportedException("Collection is read-only."); #endregion @@ -80,45 +130,45 @@ T IList.this[int index] // Natively implementing frequent Linq functions avoids boxing. Add more if necessary. /// - /// Creates a from the . + /// Creates a from the . /// - /// A that contains elements from the . + /// A that contains elements from the . public List ToList() { return _list.ToList(); } /// - /// Copies the elements of the to a new array. + /// Copies the elements of the to a new array. /// - /// An array containing copies of the elements of the . + /// An array containing copies of the elements of the . public T[] ToArray() { return _list.ToArray(); } /// - /// Returns the first element of the . + /// Returns the first element of the . /// - /// The first element of the specified - /// The is empty. + /// The first element of the specified + /// The is empty. public T First() { return _list.First(); } /// - /// Returns the last element of the . + /// Returns the last element of the . /// - /// The last element of the specified - /// The is empty. + /// The last element of the specified + /// The is empty. public T Last() { return _list.Last(); } /// - /// Returns the first element of the , or a default value if no element is found. + /// Returns the first element of the , or a default value if no element is found. /// /// if source is empty; otherwise, the first element in source. public T? FirstOrDefault() @@ -127,7 +177,7 @@ public T Last() } /// - /// Returns the last element of the , or a default value if no element is found. + /// Returns the last element of the , or a default value if no element is found. /// /// if source is empty; otherwise, the last element in source. public T? LastOrDefault() @@ -219,4 +269,15 @@ public static ImmutableFrugalList Create(IEnumerable items) return new ImmutableFrugalList(list[0]); return new FrugalList(items).ToImmutableList(); } + + /// + /// Creates a new instance of containing a single specified item. + /// + /// The type of the item. + /// The single item to include in the list. + /// An containing the specified item. + public static ImmutableFrugalList Single(T item) + { + return new ImmutableFrugalList(item); + } } \ No newline at end of file diff --git a/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTestBase.cs b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTestBase.cs index 05288eb..5bce5d9 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTestBase.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTestBase.cs @@ -52,45 +52,6 @@ public void IsReadOnly_ReturnsTrue(int count) #endregion - #region Create{T} - - [Fact] - public void Create_NullArg_ThrowsArgumentNullException() - { - Assert.Throws("items", () => ImmutableFrugalList.Create(null!)); - } - - [Theory] - [MemberData(nameof(GetEnumerableTestData))] -#pragma warning disable xUnit1026 - public void Create_CreatesCorrectImmutableList(int _, int enumerableLength, int __, int ___) - { - var list = CreateEnumerable(null, enumerableLength, 0, 0).ToList(); - - var listAsFrugal = new FrugalList(list); - var listAsImmutable = listAsFrugal.ToImmutableList(); - var listAsSet = list.ToHashSet(); - var listAsEnumerable = list.Where(_ => true); - - Assert.Equal(list, ImmutableFrugalList.Create(listAsFrugal)); - Assert.Equal(list, ImmutableFrugalList.Create(listAsImmutable)); - Assert.Equal(list, ImmutableFrugalList.Create(listAsSet)); - Assert.Equal(list, ImmutableFrugalList.Create(listAsEnumerable)); - - const ModifyOperation mods = ModifyOperation.Add | ModifyOperation.Insert | ModifyOperation.Overwrite | ModifyOperation.Remove | ModifyOperation.Clear; - - foreach (var modifyEnumerable in GetModifyEnumerables(mods, CreateT)) - { - var listCopy = new List(list); - var immutable = ImmutableFrugalList.Create(listCopy); - if (modifyEnumerable(listCopy)) - Assert.NotEqual(listCopy, immutable.ToList()); - } - } -#pragma warning restore xUnit1026 - - #endregion - #region Empty [Fact] diff --git a/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs index 3c56ac3..4fedac1 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs @@ -1,5 +1,6 @@ using AnakinRaW.CommonUtilities.Collections; using System; +using System.Linq; using Xunit; // ReSharper disable InconsistentNaming @@ -52,4 +53,50 @@ public static void IndexOf() foreach (var excluded in _excludedFromIntArray) Assert.Equal(-1, collection.IndexOf(excluded)); } +} + +public class ImmutableFrugalListTest +{ + #region Create{T} + + [Fact] + public void Create_NullArg_ThrowsArgumentNullException() + { + Assert.Throws("items", () => ImmutableFrugalList.Create(null!)); + } + + [Fact] +#pragma warning disable xUnit1026 + public void Create_CreatesCorrectImmutableList() + { + var list = new[] { 1, 2, 3, 4, 5, 6 }; + + var listAsFrugal = new FrugalList(list); + var listAsImmutable = listAsFrugal.ToImmutableList(); + var listAsSet = list.ToHashSet(); + var listAsEnumerable = list.Where(_ => true); + + Assert.Equal(list, ImmutableFrugalList.Create(listAsFrugal)); + Assert.Equal(list, ImmutableFrugalList.Create(listAsImmutable)); + Assert.Equal(list, ImmutableFrugalList.Create(listAsSet)); + Assert.Equal(list, ImmutableFrugalList.Create(listAsEnumerable)); + } +#pragma warning restore xUnit1026 + + #endregion + + #region Single{T} + + [Theory] + [InlineData(null)] + [InlineData(1)] + [InlineData("123")] + public void Single_CreatesCorrectImmutableListWithOneItem(object? data) + { + var list = ImmutableFrugalList.Single(data); + Assert.Equal([data], list); + Assert.Single(list); + } + + #endregion } \ No newline at end of file From 14df72e66916b10bf54ce0b9ee0079ee178fef38 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 16 Jan 2026 19:31:11 +0100 Subject: [PATCH 38/43] Lifecycle hooks for StepRunnerPipleines (#297) * fix compile * add lifecycle hooks and tracking pipelines for better testability of pipeline execution stages --- .../src/Pipelines/StepRunnerPipelineBase.cs | 34 +++++++++++ .../test/Pipelines/ParallelPipelineTests.cs | 41 +++++++++++++ .../Pipelines/ProducerConsumerPipelineTest.cs | 34 +++++++++++ .../test/Pipelines/SequentialPipelineTests.cs | 35 +++++++++++ .../StepRunnerPipelineBaseTestBase.cs | 59 +++++++++++++++++++ .../Pipelines/StepRunnerPipelineTestBase.cs | 7 +++ .../test/Pipelines/TrackingPipelineHelper.cs | 28 +++++++++ .../FrugalList/ImmutableFrugalListTests.cs | 2 +- 8 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/CommonUtilities.SimplePipeline/test/Pipelines/TrackingPipelineHelper.cs diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs index f82afec..e53b981 100644 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs @@ -106,6 +106,7 @@ private TStepRunner EnsureRunner() /// protected sealed override async Task ExecuteAsync(CancellationToken token) { + OnExecuteStarted(); try { StepRunner.Error += OnRunnerExecutionError!; @@ -115,9 +116,42 @@ protected sealed override async Task ExecuteAsync(CancellationToken token) { StepRunner.Error -= OnRunnerExecutionError!; } + OnRunnerExecuted(); StepRunner.ExecutedSteps.ThrowStepFailureExceptionForFailedSteps(); + OnExecuteCompleted(); } + /// + /// Called when the execution of the pipeline starts, before the step runner begins processing steps. + /// + /// + /// Override this method to perform custom logic at the start of execution. + /// + protected virtual void OnExecuteStarted() + { + } + + /// + /// Called after the step runner has completed executing all steps, before checking for step failures. + /// + /// + /// Override this method to perform custom logic after the runner has executed but before failure validation. + /// + protected virtual void OnRunnerExecuted() + { + } + + /// + /// Called when the execution of the pipeline has completed successfully. + /// + /// + /// This method is only called if no is thrown. + /// Override this method to perform custom logic upon successful completion. + /// + protected virtual void OnExecuteCompleted() + { + } + /// /// Handles errors that occur during the execution of the . /// diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs index 3f67e3c..b2c5cb7 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs @@ -22,6 +22,11 @@ protected override StepRunnerPipeline CreateStepRunnerPipeline(IList step return new TestParallelPipeline(ServiceProvider, steps, null, GetWorkerCount(runnerBehavior), failFast); } + protected override StepRunnerPipeline CreateTrackingStepRunnerPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null) + { + return new TrackingParallelPipeline(ServiceProvider, steps, GetWorkerCount(runnerBehavior), failFast, callOrder, throwOnMethod); + } + #region Constructor Tests [Fact] @@ -64,4 +69,40 @@ protected override async Task> CreateRunnerSteps(CancellationToken return _steps; } } + + private class TrackingParallelPipeline : StepRunnerPipeline + { + private readonly IList _steps; + private readonly int _workerCount; + private readonly TrackingPipelineHelper _helper; + + public TrackingParallelPipeline( + IServiceProvider serviceProvider, + IList steps, + int workerCount, + bool failFast, + List callOrder, + string? throwOnMethod) + : base(serviceProvider) + { + _steps = steps; + _workerCount = workerCount; + _helper = new TrackingPipelineHelper(callOrder, throwOnMethod); + FailFast = failFast; + } + + protected override IStepRunner CreateRunner() + { + return new AsyncStepRunner(_workerCount, ServiceProvider); + } + + protected override Task> CreateRunnerSteps(CancellationToken token) + { + return Task.FromResult(_steps); + } + + protected override void OnExecuteStarted() => _helper.OnExecuteStarted(); + protected override void OnRunnerExecuted() => _helper.OnRunnerExecuted(); + protected override void OnExecuteCompleted() => _helper.OnExecuteCompleted(); + } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs index 261b9a4..c7f3a59 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs @@ -25,6 +25,11 @@ protected override ITrackingPipeline CreateTrackingPipeline(Func CreateTrackingPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null) + { + return new TrackingProducerConsumerPipeline(ServiceProvider, steps.ToAsyncEnumerable(), GetWorkerCount(runnerBehavior), failFast, callOrder, throwOnMethod); + } + private ProducerConsumerPipeline CreateConsumerPipeline(IAsyncEnumerable steps, bool failFast, RunnerBehavior runnerBehavior) { return new TestProducerConsumerPipeline(ServiceProvider, steps, null, GetWorkerCount(runnerBehavior), failFast); @@ -685,4 +690,33 @@ private class TestProducerConsumerPipelineExposed( { public ProducerConsumerStepRunner ExposedStepRunner => StepRunner; } + + private class TrackingProducerConsumerPipeline : ProducerConsumerPipeline + { + private readonly IAsyncEnumerable _steps; + private readonly TrackingPipelineHelper _helper; + + public TrackingProducerConsumerPipeline( + IServiceProvider serviceProvider, + IAsyncEnumerable steps, + int workerCount, + bool failFast, + List callOrder, + string? throwOnMethod) + : base(workerCount, serviceProvider) + { + _steps = steps; + _helper = new TrackingPipelineHelper(callOrder, throwOnMethod); + FailFast = failFast; + } + + protected override IAsyncEnumerable BuildStepsAsync(CancellationToken token) + { + return _steps; + } + + protected override void OnExecuteStarted() => _helper.OnExecuteStarted(); + protected override void OnRunnerExecuted() => _helper.OnRunnerExecuted(); + protected override void OnExecuteCompleted() => _helper.OnExecuteCompleted(); + } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs index a0fdfe0..c7ad63c 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs @@ -25,6 +25,13 @@ protected override ITrackingPipeline CreateTrackingPipeline(Func steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null) + { + if (runnerBehavior is RunnerBehavior.Concurrent) + throw new NotSupportedException("Concurrent runs are not supported"); + return new TrackingSequentialPipeline(ServiceProvider, steps, failFast, callOrder, throwOnMethod); + } + private SequentialPipeline CreateSequentialPipeline(IList steps, bool failFast) { return new TestSequentialPipeline(ServiceProvider, steps, null, failFast); @@ -64,4 +71,32 @@ protected override async Task> CreateRunnerSteps(CancellationToken return _steps; } } + + private class TrackingSequentialPipeline : SequentialPipeline + { + private readonly IList _steps; + private readonly TrackingPipelineHelper _helper; + + public TrackingSequentialPipeline( + IServiceProvider serviceProvider, + IList steps, + bool failFast, + List callOrder, + string? throwOnMethod) + : base(serviceProvider) + { + _steps = steps; + _helper = new TrackingPipelineHelper(callOrder, throwOnMethod); + FailFast = failFast; + } + + protected override Task> CreateRunnerSteps(CancellationToken token) + { + return Task.FromResult(_steps); + } + + protected override void OnExecuteStarted() => _helper.OnExecuteStarted(); + protected override void OnRunnerExecuted() => _helper.OnRunnerExecuted(); + protected override void OnExecuteCompleted() => _helper.OnExecuteCompleted(); + } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs index 2aae8fa..d749aa5 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs @@ -21,6 +21,13 @@ protected abstract StepRunnerPipelineBase CreateStepRunnerPipelineBase( IList steps, bool failFast, RunnerBehavior runnerBehavior); + + protected abstract StepRunnerPipelineBase CreateTrackingPipeline( + IList steps, + bool failFast, + RunnerBehavior runnerBehavior, + List callOrder, + string? throwOnMethod = null); protected StepRunnerPipelineBase CreateStepRunnerPipelineBase(IList steps) { @@ -550,4 +557,56 @@ public async Task UsageTest_StepsWaitingForEachOther_WrongInsertionOrderHangsOnS } #endregion + + #region Lifecycle Hooks + + [Theory] + [InlineData(false, "OnExecuteStarted,StepExecuted,OnRunnerExecuted,OnExecuteCompleted")] + [InlineData(true, "OnExecuteStarted,OnRunnerExecuted")] + public async Task ExecuteAsync_LifecycleMethods(bool stepFails, string expectedCalls) + { + var callOrder = new List(); + var expected = expectedCalls.Split([','], StringSplitOptions.RemoveEmptyEntries); + + var step = new TestStep(_ => + { + if (stepFails) + throw new InvalidOperationException("Test error"); + callOrder.Add("StepExecuted"); + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = CreateTrackingPipeline([step], false, GetRandomRunBehavior(), callOrder); + + if (stepFails) + await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + else + await pipeline.RunAsync(TestContext.Current.CancellationToken); + + Assert.Equal(expected, callOrder); + } + + [Theory] + [InlineData("OnExecuteStarted", "OnExecuteStarted")] + [InlineData("OnRunnerExecuted", "OnExecuteStarted,StepExecuted,OnRunnerExecuted")] + [InlineData("OnExecuteCompleted", "OnExecuteStarted,StepExecuted,OnRunnerExecuted,OnExecuteCompleted")] + public async Task ExecuteAsync_LifecycleMethodThrows_ExceptionPropagates(string methodToThrow, string expectedCalls) + { + var callOrder = new List(); + var expected = expectedCalls.Split([','], StringSplitOptions.RemoveEmptyEntries); + + var step = new TestStep(_ => + { + callOrder.Add("StepExecuted"); + return Task.CompletedTask; + }, ServiceProvider); + + var pipeline = CreateTrackingPipeline([step], false, GetRandomRunBehavior(), callOrder, methodToThrow); + + var ex = await Assert.ThrowsAsync(() => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + Assert.Equal($"{methodToThrow} threw", ex.Message); + Assert.Equal(expected, callOrder); + } + #endregion } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs index 8bb72cd..3e79ce0 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs @@ -15,6 +15,13 @@ protected override StepRunnerPipelineBase CreateStepRunnerPipelineB return CreateStepRunnerPipeline(steps, failFast, runnerBehavior); } + protected abstract StepRunnerPipeline CreateTrackingStepRunnerPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null); + + protected override StepRunnerPipelineBase CreateTrackingPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null) + { + return CreateTrackingStepRunnerPipeline(steps, failFast, runnerBehavior, callOrder, throwOnMethod); + } + #region CreateRunner [Fact] diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/TrackingPipelineHelper.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/TrackingPipelineHelper.cs new file mode 100644 index 0000000..1374732 --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/TrackingPipelineHelper.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; + +internal sealed class TrackingPipelineHelper(List callOrder, string? throwOnMethod) +{ + public void OnExecuteStarted() + { + callOrder.Add("OnExecuteStarted"); + if (throwOnMethod == "OnExecuteStarted") + throw new InvalidOperationException("OnExecuteStarted threw"); + } + + public void OnRunnerExecuted() + { + callOrder.Add("OnRunnerExecuted"); + if (throwOnMethod == "OnRunnerExecuted") + throw new InvalidOperationException("OnRunnerExecuted threw"); + } + + public void OnExecuteCompleted() + { + callOrder.Add("OnExecuteCompleted"); + if (throwOnMethod == "OnExecuteCompleted") + throw new InvalidOperationException("OnExecuteCompleted threw"); + } +} diff --git a/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs index 4fedac1..f3b35f4 100644 --- a/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs +++ b/src/CommonUtilities/test/Collections/FrugalList/ImmutableFrugalListTests.cs @@ -62,7 +62,7 @@ public class ImmutableFrugalListTest [Fact] public void Create_NullArg_ThrowsArgumentNullException() { - Assert.Throws("items", () => ImmutableFrugalList.Create(null!)); + Assert.Throws("items", () => ImmutableFrugalList.Create(null!)); } [Fact] From 14d78f069c4030f863cd9c129b1a27f4b0389f59 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Fri, 16 Jan 2026 20:45:49 +0100 Subject: [PATCH 39/43] Customizable runner pipeline (#298) * fix compile * add lifecycle hooks and tracking pipelines for better testability of pipeline execution stages * fix doc * add thread safety notes to AsyncStepRunner and ProducerConsumerStepRunner docs --- .../src/Providers/DownloadProviderBase.cs | 2 +- .../src/Runners/AsyncStepRunner.cs | 6 ++++++ .../src/Runners/ProducerConsumerStepRunner.cs | 6 ++++++ .../test/Runners/StepRunnerTestBase.cs | 3 +++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/CommonUtilities.DownloadManager/src/Providers/DownloadProviderBase.cs b/src/CommonUtilities.DownloadManager/src/Providers/DownloadProviderBase.cs index e85fcdc..c3800df 100644 --- a/src/CommonUtilities.DownloadManager/src/Providers/DownloadProviderBase.cs +++ b/src/CommonUtilities.DownloadManager/src/Providers/DownloadProviderBase.cs @@ -68,7 +68,7 @@ public Task DownloadAsync( /// /// Concrete implementation for downloading a file. /// - /// DownloadAsync time and bit rate is automatically set after this method returns. + /// Download time and bit rate is automatically set after this method returns. /// The location of the source file. /// The output stream. /// Progress with already updated performance data. diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs index 6182e70..0a5010a 100644 --- a/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs +++ b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs @@ -13,8 +13,14 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; /// Represents an asynchronous step runner that manages the execution of steps in a pipeline. /// /// +/// /// This class provides functionality to add steps, execute them asynchronously, and handle errors during execution. /// It supports multiple workers for parallel step execution and ensures proper cancellation and error handling. +/// +/// +/// Thread Safety: This class and its derivatives are not thread-safe for concurrent operations +/// such as adding steps from a different thread while step execution is processing. +/// /// public class AsyncStepRunner : IStepRunner { diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs index 94ad35a..eb4f52f 100644 --- a/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs +++ b/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs @@ -16,6 +16,12 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners; /// Call to signal completion of step additions, /// otherwise the runner will block indefinitely unless cancelled via . /// +/// +/// Thread Safety: While this class supports adding steps while the runner is executing, it is not +/// thread-safe by design. Adding steps from different threads or concurrently with cancellation/error handling +/// may lead to race conditions where the consequence might be that steps added after cancellation or an error +/// might still get scheduled for execution. +/// /// public class ProducerConsumerStepRunner(int workerCount, IServiceProvider serviceProvider) : AsyncStepRunner(workerCount, serviceProvider) diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs index ad95334..aa9d575 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs @@ -1166,6 +1166,9 @@ public async Task Error_SetCancelToTrue_StepAddedAfterError_IsNotExecuted() errorOccurred.Wait(TestContext.Current.CancellationToken); + // Give the runner time to process the error and cancel + await Task.Delay(200, TestContext.Current.CancellationToken); + var step2 = new TestStep(_ => { step2Executed = true; From 4fb2c23db85913f48ca413a215c17d2ff6991a14 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 18 Jan 2026 19:05:58 +0100 Subject: [PATCH 40/43] Improved error handling & awaiting in SimplePipeline - IStep receives IsCancelled; error handling in PipelineStep refined - New methods IsExceptionType() and FindException for exception detection - StopRunnerException is now public, with documentation and constructors - AsyncStepRunner and PipelineStep support ConfigureAwait(bool) and multiple awaiting - Test classes renamed and test structure unified; test data modularized - Tests for Awaiter, ConfigureAwait and error cases significantly extended - SourceLink package removed from test project - Various minor corrections and improvements in documentation and logging --- .../src/Extensions.cs | 30 +- .../src/IStep.cs | 14 +- .../src/Pipelines/StepRunnerPipelineBase.cs | 24 +- .../src/Runners/AsyncStepRunner.cs | 36 ++- .../src/Steps/PipelineStep.cs | 62 +++- .../src/Steps/WaitStep.cs | 4 +- .../src/StopRunnerException.cs | 29 +- ...CommonUtilities.SimplePipeline.Test.csproj | 4 - .../test/ConfigureAwaitTestExtensions.cs | 115 +++++++ .../test/ExtensionsTest.cs | 229 ++++++++------ .../test/Pipelines/PipelineTest.cs | 2 +- ...pelineTestBase.cs => PipelineTestSuite.cs} | 2 +- .../Pipelines/ProducerConsumerPipelineTest.cs | 2 +- ....cs => StepRunnerPipelineBaseTestSuite.cs} | 3 +- .../Pipelines/StepRunnerPipelineTestBase.cs | 2 +- .../test/Runners/AsyncStepRunnerTest.cs | 2 +- .../Runners/ProducerConsumerStepRunnerTest.cs | 2 +- .../test/Runners/SequentialStepRunnerTest.cs | 2 +- ...nnerTestBase.cs => StepRunnerTestSuite.cs} | 139 ++++++-- .../test/Steps/PipelineStepTest.cs | 296 +++--------------- ...epTestBase.cs => PipelineStepTestSuite.cs} | 85 +++-- .../test/Steps/RunPipelineStepTest.cs | 4 +- .../test/Steps/WaitStepTest.cs | 5 +- .../test/StopRunnerExceptionTests.cs | 22 ++ .../test/TestData/TestStep.cs | 11 + src/CommonUtilities/src/AwaitExtensions.cs | 1 - 26 files changed, 680 insertions(+), 447 deletions(-) create mode 100644 src/CommonUtilities.SimplePipeline/test/ConfigureAwaitTestExtensions.cs rename src/CommonUtilities.SimplePipeline/test/Pipelines/{PipelineTestBase.cs => PipelineTestSuite.cs} (99%) rename src/CommonUtilities.SimplePipeline/test/Pipelines/{StepRunnerPipelineBaseTestBase.cs => StepRunnerPipelineBaseTestSuite.cs} (99%) rename src/CommonUtilities.SimplePipeline/test/Runners/{StepRunnerTestBase.cs => StepRunnerTestSuite.cs} (92%) rename src/CommonUtilities.SimplePipeline/test/Steps/{PipelineStepTestBase.cs => PipelineStepTestSuite.cs} (78%) create mode 100644 src/CommonUtilities.SimplePipeline/test/StopRunnerExceptionTests.cs diff --git a/src/CommonUtilities.SimplePipeline/src/Extensions.cs b/src/CommonUtilities.SimplePipeline/src/Extensions.cs index 946ace9..5d5637f 100644 --- a/src/CommonUtilities.SimplePipeline/src/Extensions.cs +++ b/src/CommonUtilities.SimplePipeline/src/Extensions.cs @@ -1,28 +1,10 @@ using System; -using System.Collections.Generic; using System.Linq; namespace AnakinRaW.CommonUtilities.SimplePipeline; internal static class Extensions { - /// - /// Throws a if any of the provided steps have failed. - /// - /// The collection of executed steps to evaluate for failures. - /// - /// Thrown when one or more steps in have failed, - /// excluding those which represent a cancelled Step. - /// - internal static void ThrowStepFailureExceptionForFailedSteps(this IEnumerable executedSteps) - { - var failedBuildSteps = executedSteps - .Where(p => p.Error != null && !p.Error.IsExceptionType()) - .ToList(); - if (failedBuildSteps.Count > 0) - throw new StepFailureException(failedBuildSteps); - } - extension(Exception error) { internal bool IsExceptionType() where T : Exception @@ -35,5 +17,17 @@ internal bool IsExceptionType() where T : Exception _ => false }; } + + internal T? FindException() where T : Exception + { + return error switch + { + T t => t, + AggregateException aggregateException => aggregateException.InnerExceptions + .Select(p => p.FindException()) + .FirstOrDefault(p => p is not null), + _ => null + }; + } } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/IStep.cs b/src/CommonUtilities.SimplePipeline/src/IStep.cs index 905bd89..a9e6ef6 100644 --- a/src/CommonUtilities.SimplePipeline/src/IStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/IStep.cs @@ -11,8 +11,20 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline; public interface IStep : IDisposable { /// - /// The exception, if any, that happened while running this step. + /// Gets a value indicating whether the step has been cancelled. /// + /// + /// if the step was cancelled; otherwise, . + /// + public bool IsCancelled { get; } + + /// + /// Gets the exception that occurred during the execution of the step, if any. + /// + /// + /// An representing the error that occurred during the step's execution, + /// or if no error occurred. + /// Exception? Error { get; } /// diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs index e53b981..c165aaa 100644 --- a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs +++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -117,10 +119,30 @@ protected sealed override async Task ExecuteAsync(CancellationToken token) StepRunner.Error -= OnRunnerExecutionError!; } OnRunnerExecuted(); - StepRunner.ExecutedSteps.ThrowStepFailureExceptionForFailedSteps(); + + var failedSteps = GetFailedSteps(StepRunner.ExecutedSteps).ToList(); + if (failedSteps.Count > 0) + throw new StepFailureException(failedSteps); OnExecuteCompleted(); } + /// + /// Retrieves the steps that have failed during execution. + /// + /// The collection of steps to evaluate for failures. + /// + /// A collection of steps that encountered errors during execution. + /// Each step in the returned collection has a non-null property. + /// + /// + /// This method filters the provided steps to identify those that have an associated error. + /// Derived classes can override this method to customize the logic for determining failed steps. + /// + protected virtual IEnumerable GetFailedSteps(IEnumerable steps) + { + return steps.Where(step => step is { Error: not null }); + } + /// /// Called when the execution of the pipeline starts, before the step runner begins processing steps. /// diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs index 0a5010a..cc2880f 100644 --- a/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs +++ b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs @@ -31,6 +31,8 @@ public class AsyncStepRunner : IStepRunner private readonly ConcurrentBag _executedSteps = []; private readonly ConcurrentBag _exceptions = []; private readonly TaskCompletionSource _completionSource = new(); + + private Task? _cachedAwaitableTask; /// public AggregateException? Exception => _exceptions.IsEmpty ? null : new AggregateException(_exceptions); @@ -102,13 +104,37 @@ public Task RunAsync(CancellationToken token) /// public TaskAwaiter GetAwaiter() { - var task = _completionSource.Task; - return task.IsCompleted - ? task.Result.GetAwaiter() - : GetAwaitableTask().GetAwaiter(); + return GetRunnerTask().GetAwaiter(); } - private async Task GetAwaitableTask() + /// + /// Configures an awaiter used to await this runner. + /// + /// + /// to attempt to marshal the continuation back to the original context captured; + /// otherwise, . + /// + /// An object used to await this runner. + public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) + { + return GetRunnerTask().ConfigureAwait(continueOnCapturedContext); + } + + private Task GetRunnerTask() + { + var tcsTask = _completionSource.Task; + if (tcsTask is { IsCompleted: true, Status: TaskStatus.RanToCompletion }) + return tcsTask.Result; + + if (_cachedAwaitableTask is not null) + return _cachedAwaitableTask; + + var newTask = CreateAwaitableTask(); + return Interlocked.CompareExchange(ref _cachedAwaitableTask, newTask, null) ?? newTask; + } + + + private async Task CreateAwaitableTask() { var task = await _completionSource.Task.ConfigureAwait(false); await task.ConfigureAwait(false); diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs index 6726ec5..e4001b3 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -14,6 +13,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps; public abstract class PipelineStep : DisposableObject, IStep { private readonly TaskCompletionSource _completionSource = new(); + private Task? _cachedAwaitableTask; /// /// Returns the service provider of this step. @@ -26,18 +26,28 @@ public abstract class PipelineStep : DisposableObject, IStep protected readonly ILogger? Logger; /// - /// Gets the exception that occurred during the execution of the step, - /// or if the step completed successfully. + /// Gets the exception that occurred during the execution of the step, if any. /// /// + /// /// If the step is cancelled by an /// (which may also be wrapped inside an ), /// this property contains the underlying cause of the cancellation when available, /// otherwise it may be . + /// + /// + /// If the step throws a , this property is always . + /// This is because a does not indicate a failure of the step itself. + /// + /// /// For all other failures, this property contains the exception that caused /// the step to fail. + /// /// - public Exception? Error { get; internal set; } + public Exception? Error { get; private set; } + + /// + public bool IsCancelled { get; private set; } /// /// Initializes a new instance of the class. @@ -61,12 +71,20 @@ public Task RunAsync(CancellationToken token) /// public TaskAwaiter GetAwaiter() { - var tcsTask = _completionSource.Task; - return tcsTask is { IsCompleted: true, Status: TaskStatus.RanToCompletion } - ? tcsTask.Result.GetAwaiter() - : GetAwaitableTask().GetAwaiter(); + return GetStepTask().GetAwaiter(); } - + + /// + /// + /// + /// + /// + public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) + { + return GetStepTask().ConfigureAwait(continueOnCapturedContext); + } + + /// /// Returns a string that represents the current instance. /// @@ -84,12 +102,25 @@ public override string ToString() /// Provided to allow cancellation. protected abstract Task RunCoreAsync(CancellationToken token); - private async Task GetAwaitableTask() + private async Task CreateAwaitableTask() { var task = await _completionSource.Task.ConfigureAwait(false); await task.ConfigureAwait(false); } + private Task GetStepTask() + { + var tcsTask = _completionSource.Task; + if (tcsTask is { IsCompleted: true, Status: TaskStatus.RanToCompletion }) + return tcsTask.Result; + + if (_cachedAwaitableTask is not null) + return _cachedAwaitableTask; + + var newTask = CreateAwaitableTask(); + return Interlocked.CompareExchange(ref _cachedAwaitableTask, newTask, null) ?? newTask; + } + private async Task ExecuteStepAsync(CancellationToken token) { Logger?.LogTrace("BEGIN: {Step}", this); @@ -101,6 +132,7 @@ private async Task ExecuteStepAsync(CancellationToken token) catch (OperationCanceledException ex) { Error = ex.InnerException; + IsCancelled = true; throw; } catch (StopRunnerException) @@ -109,13 +141,17 @@ private async Task ExecuteStepAsync(CancellationToken token) } catch (AggregateException e) { - if (!e.IsExceptionType()) + if (e.IsExceptionType()) + { + Error = e.FindException()?.InnerException; + IsCancelled = true; + } + else { Error = e; LogFaultException(e); } - else - Error = e.InnerExceptions.FirstOrDefault(p => p.IsExceptionType())?.InnerException; + throw; } catch (Exception e) diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs index 52bbb39..8f2b4dd 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs @@ -26,7 +26,7 @@ public WaitStep(IStepRunner stepRunner, IServiceProvider serviceProvider) : base /// [ExcludeFromCodeCoverage] - public override string ToString() => "Waiting for other steps"; + public override string ToString() => "Waiting for other step runner"; /// /// Waits for the instance's parallel stepRunner. @@ -38,7 +38,7 @@ protected override async Task RunCoreAsync(CancellationToken token) await _stepRunner; if (_stepRunner.Exception is not null) { - Logger?.LogTrace("The awaited step runner has exceptions. Stopping all subsequent steps."); + Logger?.LogTrace("The other step runner finished with failed steps. Stopping current runner."); throw new StopRunnerException(); } } diff --git a/src/CommonUtilities.SimplePipeline/src/StopRunnerException.cs b/src/CommonUtilities.SimplePipeline/src/StopRunnerException.cs index 645e269..2154c29 100644 --- a/src/CommonUtilities.SimplePipeline/src/StopRunnerException.cs +++ b/src/CommonUtilities.SimplePipeline/src/StopRunnerException.cs @@ -2,4 +2,31 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline; -internal class StopRunnerException : Exception; \ No newline at end of file +/// +/// The exception that is thrown to signal an the termination of step execution. +/// +/// +/// This exception is typically thrown by an to indicate that its associated +/// should stop executing any further steps. +/// +public sealed class StopRunnerException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor creates a default instance of the exception without any additional context or message. + /// It is typically used to signal the termination of step execution in a pipeline. + /// + public StopRunnerException() : base("Stopping step runner.") + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public StopRunnerException(string? message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj index fed10fe..4dc7d23 100644 --- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj +++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj @@ -38,8 +38,4 @@ - - - - diff --git a/src/CommonUtilities.SimplePipeline/test/ConfigureAwaitTestExtensions.cs b/src/CommonUtilities.SimplePipeline/test/ConfigureAwaitTestExtensions.cs new file mode 100644 index 0000000..8526973 --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/ConfigureAwaitTestExtensions.cs @@ -0,0 +1,115 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test; + +// Based on https://github.com/dotnet/runtime TaskAwaiterTests.cs + +public static class ConfigureAwaitTestExtensions +{ + public static void AwaiterAndAwaitableEquality( + Func instanceFactory, + Func awaitableFactory, + Func configuredAwaitableFactory) + { + var instance = instanceFactory(); + + // TaskAwaiter + Assert.Equal(awaitableFactory(instance), awaitableFactory(instance)); + + // ConfiguredTaskAwaitable + Assert.Equal(configuredAwaitableFactory(instance, false), configuredAwaitableFactory(instance, false)); + Assert.NotEqual(configuredAwaitableFactory(instance, false), configuredAwaitableFactory(instance, true)); + Assert.NotEqual(configuredAwaitableFactory(instance, true), configuredAwaitableFactory(instance, false)); + + // ConfiguredTaskAwaitable.ConfiguredTaskAwaiter + Assert.Equal(configuredAwaitableFactory(instance, false).GetAwaiter(), configuredAwaitableFactory(instance, false).GetAwaiter()); + Assert.NotEqual(configuredAwaitableFactory(instance, false).GetAwaiter(), configuredAwaitableFactory(instance, true).GetAwaiter()); + Assert.NotEqual(configuredAwaitableFactory(instance, true).GetAwaiter(), configuredAwaitableFactory(instance, false).GetAwaiter()); + } + + public static void TestOnCompletedCompletesInAnotherSynchronizationContext( + bool? continueOnCapturedContext, + Func instanceFactory, + Func awaitableFactory, + Func configuredAwaitableFactory, + Action completeInstance) + { + var origCtx = SynchronizationContext.Current; + try + { + var validateCtx = new ValidateCorrectContextSynchronizationContext(); + Assert.Equal(0, validateCtx.PostCount); + SynchronizationContext.SetSynchronizationContext(validateCtx); + + var mre = new ManualResetEventSlim(); + + var instance = instanceFactory(); + + // Hook up a callback + var postedInContext = false; + var callback = () => + { + postedInContext = ValidateCorrectContextSynchronizationContext.IsPostedInContext; + mre.Set(); + }; + + if (continueOnCapturedContext.HasValue) + configuredAwaitableFactory(instance, continueOnCapturedContext.Value).GetAwaiter().OnCompleted(callback); + else + awaitableFactory(instance).OnCompleted(callback); + + Assert.False(mre.IsSet, "Callback should not yet have run."); + + // Complete the task in another context and wait for the callback to run + Task.Run(() => completeInstance(instance), TestContext.Current.CancellationToken); + mre.Wait(TestContext.Current.CancellationToken); + + // Validate the callback ran and in the correct context + var shouldHavePosted = !continueOnCapturedContext.HasValue || continueOnCapturedContext.Value; + Assert.Equal(shouldHavePosted ? 1 : 0, validateCtx.PostCount); + Assert.Equal(shouldHavePosted, postedInContext); + } + finally + { + SynchronizationContext.SetSynchronizationContext(origCtx); + } + } + + private class ValidateCorrectContextSynchronizationContext : SynchronizationContext + { + [ThreadStatic] + internal static bool IsPostedInContext; + + internal int PostCount; + private int _sendCount; + + public override void Post(SendOrPostCallback d, object? state) + { + Interlocked.Increment(ref PostCount); + Task.Run(() => + { + SetSynchronizationContext(this); + try + { + IsPostedInContext = true; + d(state); + } + finally + { + IsPostedInContext = false; + SetSynchronizationContext(null); + } + }); + } + + public override void Send(SendOrPostCallback d, object? state) + { + Interlocked.Increment(ref _sendCount); + d(state); + } + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs b/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs index b6810a3..dc2ac2a 100644 --- a/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs @@ -2,118 +2,167 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.Testing.Extensions; +using System.Reflection; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test; public class ExtensionsTest : TestBaseWithServiceProvider -{ - [Fact] - public void IsExceptionType() +{ + public static IEnumerable ExceptionSearchTestCases() { - var i = new InvalidOperationException(); - - Assert.True(i.IsExceptionType()); - Assert.False(i.IsExceptionType()); - - var io = new IOException(); - Assert.True(io.IsExceptionType()); - Assert.True(io.IsExceptionType()); - - var a = new AggregateException(new List { i, io }); - Assert.True(a.IsExceptionType()); - Assert.True(a.IsExceptionType()); - Assert.True(a.IsExceptionType()); - - var n = new NullReferenceException(); - var da = new AggregateException(a, n); - Assert.True(da.IsExceptionType()); - Assert.True(da.IsExceptionType()); - Assert.True(da.IsExceptionType()); - Assert.True(da.IsExceptionType()); - } + // Direct match - same type + { + var ex = new InvalidOperationException("test"); + yield return [ex, typeof(InvalidOperationException), ex]; + } + { + var ex = new IOException("test"); + yield return [ex, typeof(IOException), ex]; + } - [Fact] - public void ThrowStepFailureExceptionForFailedSteps_DoesNotThrowIfEmpty() - { - IEnumerable steps = []; - var exception = Record.Exception(() => steps.ThrowStepFailureExceptionForFailedSteps()); - Assert.Null(exception); - } + // Direct match - base type + { + var ex = new InvalidOperationException("test"); + yield return [ex, typeof(Exception), ex]; + } + { + var ex = new IOException("test"); + yield return [ex, typeof(Exception), ex]; + } - [Fact] - public void ThrowStepFailureExceptionForFailedSteps_DoesNotThrowIfNoStepFailed() - { - var step1 = new ErrorStep { Error = null }; - var step2 = new ErrorStep { Error = new OperationCanceledException() }; - var step3 = new ErrorStep { Error = new AggregateException(new OperationCanceledException()) }; - var step4 = new ErrorStep { Error = new AggregateException(new Exception(), new OperationCanceledException()) }; - - IEnumerable steps = [step1, step2, step3, step4]; - var exception = Record.Exception(() => steps.ThrowStepFailureExceptionForFailedSteps()); - Assert.Null(exception); - } + // No match + yield return [new InvalidOperationException("test"), typeof(IOException), null]; + yield return [new IOException("test"), typeof(InvalidOperationException), null]; + yield return [new InvalidOperationException("test"), typeof(NullReferenceException), null]; - private static IEnumerable GetNonFailedSteps() - { - yield return new ErrorStep { Error = null }; - yield return new ErrorStep { Error = new OperationCanceledException() }; - yield return new ErrorStep { Error = new TaskCanceledException() }; - yield return new ErrorStep { Error = new AggregateException(new OperationCanceledException()) }; - yield return new ErrorStep { Error = new AggregateException(new TaskCanceledException()) }; - yield return new ErrorStep { Error = new AggregateException(new AggregateException(new OperationCanceledException())) }; - yield return new ErrorStep { Error = new AggregateException(new Exception(), new OperationCanceledException()) }; + // Simple AggregateException + { + var invalidOp = new InvalidOperationException("test"); + var ioEx = new IOException("test"); + var agg = new AggregateException(invalidOp, ioEx); + yield return [agg, typeof(AggregateException), agg]; + yield return [agg, typeof(InvalidOperationException), invalidOp]; + yield return [agg, typeof(IOException), ioEx]; + yield return [agg, typeof(NullReferenceException), null]; + } + + // Nested AggregateException + { + var invalidOp = new InvalidOperationException("test"); + var ioEx = new IOException("test"); + var nullRef = new NullReferenceException("test"); + var innerAgg = new AggregateException(invalidOp, ioEx); + var outerAgg = new AggregateException(innerAgg, nullRef); + yield return [outerAgg, typeof(AggregateException), outerAgg]; + yield return [outerAgg, typeof(InvalidOperationException), invalidOp]; + yield return [outerAgg, typeof(IOException), ioEx]; + yield return [outerAgg, typeof(NullReferenceException), nullRef]; + yield return [outerAgg, typeof(ArgumentException), null]; + } + + // A(A(OCE)) - OCE without inner exception + { + var oce = new OperationCanceledException("test"); + yield return [new AggregateException(new AggregateException(oce)), typeof(OperationCanceledException), oce]; + } + + // A(A(OCE(E))) - OCE with inner exception + { + var oce = new OperationCanceledException("test", new TimeoutException("test")); + var agg = new AggregateException(new AggregateException(oce)); + yield return [agg, typeof(OperationCanceledException), oce]; + yield return [agg, typeof(TimeoutException), null]; // Not found via InnerExceptions! + } + + // Deeply nested + { + var oce = new OperationCanceledException("test", new TimeoutException("test")); + var agg = new AggregateException( + new AggregateException( + new AggregateException( + new AggregateException(oce)))); + yield return [agg, typeof(OperationCanceledException), oce]; + yield return [agg, typeof(TimeoutException), null]; // Not found via InnerExceptions! + yield return [agg, typeof(ArgumentException), null]; + } } - private static IEnumerable GetFailedSteps() + public static IEnumerable OceInnerExceptionTestCases() { - yield return new ErrorStep { Error = new Exception() }; - yield return new ErrorStep { Error = new AggregateException(new ArgumentException()) }; + // Direct OCE without inner + yield return [new OperationCanceledException("test"), null]; + + // Direct OCE with inner + { + var timeout = new TimeoutException("test"); + yield return [new OperationCanceledException("test", timeout), timeout]; + } + + // A(A(OCE)) - no inner + yield return [new AggregateException(new AggregateException(new OperationCanceledException("test"))), null]; + + // A(A(OCE(E))) - with inner + { + var timeout = new TimeoutException("test"); + yield return [new AggregateException(new AggregateException(new OperationCanceledException("test", timeout))), timeout]; + } + + // Deeply nested with inner + { + var timeout = new TimeoutException("test"); + yield return [new AggregateException( + new AggregateException( + new AggregateException( + new AggregateException( + new OperationCanceledException("test", timeout))))), timeout]; + } } - - [Fact] - public void ThrowStepFailureExceptionForFailedSteps_ThrowsIfOneStepFailed() + + [Theory] + [MemberData(nameof(ExceptionSearchTestCases))] + public void IsExceptionType_ReturnsExpectedResult( + Exception source, + Type searchType, + Exception? expectedFound) { - var exception = Assert.Throws(() => - GetNonFailedSteps().Append(GetFailedSteps().First()) - .ThrowStepFailureExceptionForFailedSteps()); - Assert.Single(exception.FailedSteps); + var method = typeof(Extensions) + .GetMethod(nameof(Extensions.IsExceptionType), BindingFlags.Static | BindingFlags.NonPublic)! + .MakeGenericMethod(searchType); + + var result = (bool)method.Invoke(null, [source])!; + + Assert.Equal(expectedFound is not null, result); } - [Fact] - public void ThrowStepFailureExceptionForFailedSteps_ThrowsIfAllStepsFailed() + [Theory] + [MemberData(nameof(ExceptionSearchTestCases))] + public void FindException_ReturnsExpectedResult(Exception source, Type searchType, Exception? expectedFound) { - var failed = GetFailedSteps().ToList(); - var exception = Assert.Throws(() => failed.ThrowStepFailureExceptionForFailedSteps()); - Assert.EqualUnordered(failed, exception.FailedSteps.ToList()); + var method = typeof(Extensions) + .GetMethod(nameof(Extensions.FindException), BindingFlags.Static | BindingFlags.NonPublic)! + .MakeGenericMethod(searchType); + + var result = (Exception?)method.Invoke(null, [source]); + + Assert.Same(expectedFound, result); } - [Fact] - public void ThrowStepFailureExceptionForFailedSteps_ThrowsIfManyStepsFailed() + [Theory] + [MemberData(nameof(OceInnerExceptionTestCases))] + public void FindException_OperationCanceledException_ReturnsCorrectInnerException(Exception source, Exception? expectedInner) { - var failed = GetFailedSteps().ToList(); - var exception = Assert.Throws(() => failed.Concat(GetNonFailedSteps()).ThrowStepFailureExceptionForFailedSteps()); - Assert.All(exception.FailedSteps, step => - { - Assert.Contains(step, failed); - }); + var found = source.FindException(); + Assert.Same(expectedInner, found?.InnerException); } - private class ErrorStep : IStep + [Fact] + public void FindException_ReturnsFirstMatch_WhenMultipleExist() { - public Exception? Error { get; init; } - - public void Dispose() - { - } - - public Task RunAsync(CancellationToken token) => throw new NotImplementedException(); - - public TaskAwaiter GetAwaiter() => throw new NotImplementedException(); + var io1 = new IOException("First"); + var io2 = new IOException("Second"); + var aggregate = new AggregateException(io1, io2); + var found = aggregate.FindException(); + Assert.Same(io1, found); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs index 890efe6..1a564a0 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTest.cs @@ -5,7 +5,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -public class PipelineTest : PipelineTestBase +public class PipelineTest : PipelineTestSuite { protected override Pipeline CreatePipeline(IList steps) { diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestSuite.cs similarity index 99% rename from src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestBase.cs rename to src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestSuite.cs index 79dc947..5c78249 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/PipelineTestSuite.cs @@ -8,7 +8,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -public abstract class PipelineTestBase : TestBaseWithServiceProvider +public abstract class PipelineTestSuite : TestBaseWithServiceProvider { protected abstract Pipeline CreatePipeline(IList steps); diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs index c7f3a59..88b909c 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs @@ -12,7 +12,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -public class ProducerConsumerPipelineTest : StepRunnerPipelineBaseTestBase +public class ProducerConsumerPipelineTest : StepRunnerPipelineBaseTestSuite { protected override StepRunnerPipelineBase CreateStepRunnerPipelineBase(IList steps, bool failFast, RunnerBehavior runnerBehavior) { diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs similarity index 99% rename from src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs rename to src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs index d749aa5..254eb54 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -11,7 +12,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -public abstract class StepRunnerPipelineBaseTestBase : PipelineTestBase where TRunner : class, IStepRunner +public abstract class StepRunnerPipelineBaseTestSuite : PipelineTestSuite where TRunner : class, IStepRunner { protected virtual bool RunnerSupportsSequentialRuns => true; diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs index 3e79ce0..2da1283 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs @@ -6,7 +6,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Pipelines; -public abstract class StepRunnerPipelineTestBase : StepRunnerPipelineBaseTestBase +public abstract class StepRunnerPipelineTestBase : StepRunnerPipelineBaseTestSuite { protected abstract StepRunnerPipeline CreateStepRunnerPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior); diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/AsyncStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/AsyncStepRunnerTest.cs index 4b691f5..ba5a6db 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/AsyncStepRunnerTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/AsyncStepRunnerTest.cs @@ -2,7 +2,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; -public class AsyncStepRunnerTest : StepRunnerTestBase +public class AsyncStepRunnerTest : StepRunnerTestSuite { public override bool HasSequentialStepExecutionOrder => false; diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/ProducerConsumerStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/ProducerConsumerStepRunnerTest.cs index d75912e..99b6412 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/ProducerConsumerStepRunnerTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/ProducerConsumerStepRunnerTest.cs @@ -8,7 +8,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; -public class ProducerConsumerStepRunnerTest : StepRunnerTestBase +public class ProducerConsumerStepRunnerTest : StepRunnerTestSuite { public override bool HasSequentialStepExecutionOrder => false; diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/SequentialStepRunnerTest.cs b/src/CommonUtilities.SimplePipeline/test/Runners/SequentialStepRunnerTest.cs index 6449546..5c8934c 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/SequentialStepRunnerTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/SequentialStepRunnerTest.cs @@ -4,7 +4,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; -public class SequentialStepRunnerTest : StepRunnerTestBase +public class SequentialStepRunnerTest : StepRunnerTestSuite { public override bool HasSequentialStepExecutionOrder => true; diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestSuite.cs similarity index 92% rename from src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs rename to src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestSuite.cs index aa9d575..e04909a 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestSuite.cs @@ -10,7 +10,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Runners; -public abstract class StepRunnerTestBase : TestBaseWithServiceProvider where T : AsyncStepRunner +public abstract class StepRunnerTestSuite : TestBaseWithServiceProvider where T : AsyncStepRunner { /// /// Indicates whether the runner guarantees sequential step execution order. @@ -1445,7 +1445,7 @@ public void WaitWithTimeout_RunnerNeverRuns_ThrowsTimeoutException() #endregion - #region GetAwaiter Tests + #region GetAwaiter / ConfigureAwait [Fact] public async Task GetAwaiter_BeforeRun_WaitsUntilRunnerStartedAndCompleted() @@ -1463,14 +1463,21 @@ public async Task GetAwaiter_BeforeRun_WaitsUntilRunnerStartedAndCompleted() FinishAdding(runner); var awaitTask = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + var awaitTaskConfigureAwaitT = Task.Run(async () => await runner.ConfigureAwait(true), TestContext.Current.CancellationToken); + var awaitTaskConfigureAwaitF = Task.Run(async () => await runner.ConfigureAwait(false), TestContext.Current.CancellationToken); await Task.Delay(50, TestContext.Current.CancellationToken); Assert.False(awaitTask.IsCompleted, "Awaiter should block until runner starts and completes"); await runner.RunAsync(CancellationToken.None); - var completed = await WaitForTaskWithTimeout(awaitTask, TimeSpan.FromSeconds(5)); - Assert.True(completed, "Await task should complete after runner finishes"); + await awaitTask; + await awaitTaskConfigureAwaitT; + await awaitTaskConfigureAwaitF; + + Assert.True(awaitTask.IsCompleted); + Assert.True(awaitTaskConfigureAwaitT.IsCompleted); + Assert.True(awaitTaskConfigureAwaitF.IsCompleted); Assert.True(stepExecuted); } @@ -1493,17 +1500,27 @@ public async Task GetAwaiter_DuringRun_WaitsForCompletion() var runTask = runner.RunAsync(CancellationToken.None); var awaitTask = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + var awaitTaskConfigureAwaitT = Task.Run(async () => await runner.ConfigureAwait(true), TestContext.Current.CancellationToken); + var awaitTaskConfigureAwaitF = Task.Run(async () => await runner.ConfigureAwait(false), TestContext.Current.CancellationToken); await Task.Delay(50, TestContext.Current.CancellationToken); Assert.False(awaitTask.IsCompleted, "Awaiter should block while runner is executing"); + Assert.False(awaitTaskConfigureAwaitT.IsCompleted, "Awaiter should block while runner is executing"); + Assert.False(awaitTaskConfigureAwaitF.IsCompleted, "Awaiter should block while runner is executing"); Assert.False(stepCompleted); tcs.SetResult(true); - var completed = await WaitForTaskWithTimeout(awaitTask, TimeSpan.FromSeconds(5)); - Assert.True(completed, "Awaiter should complete when step finishes"); - Assert.True(stepCompleted); + await awaitTask; + await awaitTaskConfigureAwaitT; + await awaitTaskConfigureAwaitF; + Assert.True(awaitTask.IsCompleted); + Assert.True(awaitTaskConfigureAwaitT.IsCompleted); + Assert.True(awaitTaskConfigureAwaitF.IsCompleted); + + Assert.True(stepCompleted); + await runTask; } @@ -1519,11 +1536,16 @@ public async Task GetAwaiter_AfterRun_CompletesImmediately() await runner.RunAsync(CancellationToken.None); var awaiter = runner.GetAwaiter(); + var awaitConfigureAwaitT = runner.ConfigureAwait(true); + var awaitConfigureAwaitF = runner.ConfigureAwait(false); + Assert.True(awaiter.IsCompleted); + Assert.True(awaitConfigureAwaitT.GetAwaiter().IsCompleted); + Assert.True(awaitConfigureAwaitF.GetAwaiter().IsCompleted); - var awaitTask = Task.Run(async () => await runner, TestContext.Current.CancellationToken); - var completed = await WaitForTaskWithTimeout(awaitTask, TimeSpan.FromSeconds(1)); - Assert.True(completed, "Awaiter should complete immediately after run"); + await runner; + await runner.ConfigureAwait(false); + await runner.ConfigureAwait(true); } [Fact] @@ -1536,8 +1558,8 @@ public async Task GetAwaiter_MultipleAwaitersBeforeRun_AllCompleteWhenRunFinishe FinishAdding(runner); var awaitTask1 = Task.Run(async () => await runner, TestContext.Current.CancellationToken); - var awaitTask2 = Task.Run(async () => await runner, TestContext.Current.CancellationToken); - var awaitTask3 = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + var awaitTask2 = Task.Run(async () => await runner.ConfigureAwait(true), TestContext.Current.CancellationToken); + var awaitTask3 = Task.Run(async () => await runner.ConfigureAwait(false), TestContext.Current.CancellationToken); await Task.Delay(50, TestContext.Current.CancellationToken); Assert.False(awaitTask1.IsCompleted); @@ -1546,10 +1568,13 @@ public async Task GetAwaiter_MultipleAwaitersBeforeRun_AllCompleteWhenRunFinishe await runner.RunAsync(CancellationToken.None); - var allCompleted = await WaitForTaskWithTimeout( - Task.WhenAll(awaitTask1, awaitTask2, awaitTask3), - TimeSpan.FromSeconds(5)); - Assert.True(allCompleted, "All awaiters should complete when runner finishes"); + await awaitTask1; + await awaitTask2; + await awaitTask3; + + Assert.True(awaitTask1.IsCompleted); + Assert.True(awaitTask2.IsCompleted); + Assert.True(awaitTask3.IsCompleted); } [Fact] @@ -1563,9 +1588,14 @@ public async Task GetAwaiter_WithStepErrors_CompletesWithoutThrowing() var runTask = runner.RunAsync(CancellationToken.None); - var exception = await Record.ExceptionAsync(async () => await runner); + var eList = new List + { + await Record.ExceptionAsync(async () => await runner), + await Record.ExceptionAsync(async () => await runner.ConfigureAwait(false)), + await Record.ExceptionAsync(async () => await runner.ConfigureAwait(true)) + }; - Assert.Null(exception); + Assert.All(eList, Assert.Null); Assert.NotNull(runner.Exception); await runTask; @@ -1580,15 +1610,19 @@ public async Task GetAwaiter_CalledMultipleTimesBeforeAndAfterRun_AllSucceed() runner.AddStep(step); FinishAdding(runner); - var awaiterBefore = runner.GetAwaiter(); - Assert.False(awaiterBefore.IsCompleted); + Assert.False(runner.GetAwaiter().IsCompleted); + Assert.False(runner.ConfigureAwait(false).GetAwaiter().IsCompleted); + Assert.False(runner.ConfigureAwait(true).GetAwaiter().IsCompleted); await runner.RunAsync(CancellationToken.None); - var awaiterAfter = runner.GetAwaiter(); - Assert.True(awaiterAfter.IsCompleted); + Assert.True(runner.GetAwaiter().IsCompleted); + Assert.True(runner.ConfigureAwait(false).GetAwaiter().IsCompleted); + Assert.True(runner.ConfigureAwait(true).GetAwaiter().IsCompleted); #pragma warning disable xUnit1031 - awaiterAfter.GetResult(); + runner.GetAwaiter().GetResult(); + runner.ConfigureAwait(true).GetAwaiter().GetResult(); + runner.ConfigureAwait(false).GetAwaiter().GetResult(); #pragma warning restore xUnit1031 } @@ -1612,7 +1646,12 @@ public async Task GetAwaiter_MultipleAwaiters_AllCompleteWhenCancelled() FinishAdding(runner); // Start multiple awaiters before cancellation - var awaitTask = Task.Run(async () => await runner, TestContext.Current.CancellationToken); + var awaiterTasks = new[] + { + Task.Run(async () => await runner, TestContext.Current.CancellationToken), + Task.Run(async () => await runner, TestContext.Current.CancellationToken), + Task.Run(async () => await runner, TestContext.Current.CancellationToken), + }; var runTask = runner.RunAsync(cts.Token); @@ -1622,12 +1661,55 @@ public async Task GetAwaiter_MultipleAwaiters_AllCompleteWhenCancelled() cts.Cancel(); canCancel.Set(); - // All awaiters should complete without throwing - var exception = await Record.ExceptionAsync(() => awaitTask); + var allAwaitersTask = Task.WhenAll(awaiterTasks); + var exception = await Record.ExceptionAsync(() => allAwaitersTask); Assert.Null(exception); + } - await runTask; + [Fact] + public void AwaiterAndAwaitableEquality() + { + ConfigureAwaitTestExtensions.AwaiterAndAwaitableEquality( + () => + { + var stepRunner = CreateStepRunner(); + return stepRunner; + }, + step => step.GetAwaiter(), + (step, ca) => step.ConfigureAwait(ca)); + + + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [InlineData(null)] + public void OnCompleted_CompletesInAnotherSynchronizationContext(bool? continueOnCapturedContext) + { + var executeCount = 0; + var step = new TestStep((_ => + { + Interlocked.Increment(ref executeCount); + return Task.CompletedTask; + }), ServiceProvider); + + ConfigureAwaitTestExtensions.TestOnCompletedCompletesInAnotherSynchronizationContext( + continueOnCapturedContext, + () => { + var stepRunner = CreateStepRunner(); + stepRunner.AddStep(step); + stepRunner.AddStep(step); + stepRunner.AddStep(step); + FinishAdding(stepRunner); + return stepRunner; + }, + runner => runner.GetAwaiter(), + (runner, ca) => runner.ConfigureAwait(ca), + runner => runner.RunAsync(CancellationToken.None)); + + Assert.Equal(3, executeCount); } #endregion @@ -2081,8 +2163,7 @@ public void RunAsync_SequentialRunner_StepAwaitsLaterStep_WouldDeadlock() } #endregion - - + // TODO: Remove private static async Task WaitForTaskWithTimeout(Task task, TimeSpan timeout) { diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs index 07cba17..a1be549 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTest.cs @@ -1,22 +1,20 @@ -using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.SimplePipeline.Steps; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.SimplePipeline.Steps; -using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class TestStepTest : PipelineStepTestBase +public class PipelineStepTest : PipelineStepTestSuite { protected override bool StepRespectsCancellationToken => true; protected override bool StepAddsExceptionsToErrorProperty => true; - protected override bool StepAddsStopRunnerExceptionToErrorProperty => false; protected override Type GetExpectedExceptionType(Exception thrownException) { - // TestStep propagates exceptions as-is return thrownException.GetType(); } @@ -30,277 +28,81 @@ protected override PipelineStep CreateStepWithAction(Func(() => new TestStep(null, null!)); - } - - [Fact] - public void ToString_IsTypeName() - { - var step = new TestStep(null, ServiceProvider); - Assert.Equal(step.GetType().Name, step.ToString()); - } -} + #region Ctor -public class PipelineStepTest : TestBaseWithServiceProvider -{ [Fact] public void Ctor_NullArgs_Throws() { Assert.Throws(() => new TestStep(null, null!)); } - [Fact] - public void Disposed() - { - var step = new TestStep(null, ServiceProvider); - - step.Dispose(); - Assert.True(step.IsDisposed); - } - - [Fact] - public async Task RunAsync_TaskAction() - { - var ran = false; - var step = new TestStep(_ => - { - ran = true; - return Task.CompletedTask; - }, ServiceProvider); + #endregion - await step.RunAsync(CancellationToken.None); - - Assert.True(ran); - } + #region ToString [Fact] - public async Task RunAsync_AwaitedAction() + public void ToString_IsTypeName() { - var ran = false; - var step = new TestStep(async _ => - { - await Task.Yield(); - ran = true; - }, ServiceProvider); - - await step.RunAsync(CancellationToken.None); - - Assert.True(ran); + var step = new TestStep(null, ServiceProvider); + Assert.Equal(step.GetType().Name, step.ToString()); } - [Fact] - public async Task RunAsync_ThrowsException() - { - var expectedError = new Exception(); - - var step = new TestStep(_ => throw expectedError, ServiceProvider); + #endregion - await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); - Assert.Same(expectedError, step.Error); - } + #region Error - [Fact] - public async Task RunAsync_WithCancellation_ThrowsOperationCanceledException() + [Theory] + [MemberData(nameof(StepsThatThrowError_TestData))] + public async Task Error_Cancel_PropertyIsCorrectlySet(PipelineStep step, bool shouldContainError, bool isCancel) { - var step = new TestStep(ct => + try { - ct.ThrowIfCancellationRequested(); - return Task.CompletedTask; - }, ServiceProvider); - - var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(() => step.RunAsync(cts.Token)); - Assert.Null(step.Error); - } - - [Fact] - public async Task RunAsync_StopRunnerException_IsNotAddedToErrors() - { - var step = new TestStep(_ => throw new StopRunnerException(), ServiceProvider); - - await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); - Assert.Null(step.Error); - } - - [Fact] - public async Task RunAsync_AggregateException() - { - var expected = new AggregateException(new Exception("Test")); - var step = new TestStep(_ => throw expected, ServiceProvider); - - await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); - Assert.Same(expected, step.Error); - } - - [Fact] - public async Task RunAsync_AggregateException_OriginatedFromOperationCancelled() - { - var expected = new Exception("Test"); - var step = new TestStep(_ => throw new AggregateException(new OperationCanceledException(null, expected)), ServiceProvider); - - await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); - Assert.Same(expected, step.Error); - } - - [Fact] - public async Task RunAsync_AggregateException_OriginatedFromOperationCancelled_NoInnerException() - { - var step = new TestStep(_ => throw new AggregateException(new OperationCanceledException()), ServiceProvider); - - await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); - Assert.Null(step.Error); - } - - [Fact] - public async Task GetAwaiter_AfterCompletion_ReturnsImmediately() - { - var executed = false; - var step = new TestStep(_ => + await step.RunAsync(TestContext.Current.CancellationToken); + } + catch { - executed = true; - return Task.CompletedTask; - }, ServiceProvider); - - await step.RunAsync(CancellationToken.None); + // Ignore + } - await step; - Assert.True(executed); - } - - [Fact] - public async Task GetAwaiter_BeforeStart_WaitsForCompletion() - { - var tcs = new TaskCompletionSource(); - var step = new TestStep(async _ => - { - await tcs.Task; - }, ServiceProvider); - - var awaiterTask = Task.Run(async () => await step, TestContext.Current.CancellationToken); - - await Task.Delay(50, TestContext.Current.CancellationToken); - Assert.False(awaiterTask.IsCompleted); - - var runTask = step.RunAsync(CancellationToken.None); - - await Task.Delay(50, TestContext.Current.CancellationToken); - Assert.False(awaiterTask.IsCompleted); - - tcs.SetResult(true); - await runTask; - - await awaiterTask; - Assert.True(awaiterTask.IsCompleted); - } - - [Fact] - public async Task GetAwaiter_DuringExecution_WaitsForCompletion() - { - var tcs = new TaskCompletionSource(); - var step = new TestStep(async _ => - { - await tcs.Task; - }, ServiceProvider); - - var runTask = step.RunAsync(CancellationToken.None); - - var awaiterTask = Task.Run(async () => await step, TestContext.Current.CancellationToken); - - await Task.Delay(50, TestContext.Current.CancellationToken); - Assert.False(awaiterTask.IsCompleted); - - tcs.SetResult(true); - await runTask; - - await awaiterTask; - Assert.True(awaiterTask.IsCompleted); - } - - [Fact] - public async Task GetAwaiter_PropagatesException() - { - var expected = new InvalidOperationException("Test error"); - var step = new TestStep(_ => throw expected, ServiceProvider); - - await Assert.ThrowsAsync(() => step.RunAsync(CancellationToken.None)); - await Assert.ThrowsAsync(async () => await step); + Assert.Equal(shouldContainError, step.Error is not null); + + Assert.Equal(isCancel, step.IsCancelled); } - [Fact] - public async Task GetAwaiter_MultipleAwaiters_AllComplete() + #endregion + + public static IEnumerable StepsThatThrowError_TestData() { - var tcs = new TaskCompletionSource(); - var step = new TestStep(async _ => - { - await tcs.Task; - }, ServiceProvider); - - var awaiter1 = Task.Run(async () => await step, TestContext.Current.CancellationToken); - var awaiter2 = Task.Run(async () => await step, TestContext.Current.CancellationToken); - var awaiter3 = Task.Run(async () => await step, TestContext.Current.CancellationToken); - - var runTask = step.RunAsync(CancellationToken.None); - - await Task.Delay(50, TestContext.Current.CancellationToken); - Assert.False(awaiter1.IsCompleted); - Assert.False(awaiter2.IsCompleted); - Assert.False(awaiter3.IsCompleted); - - tcs.SetResult(true); - await runTask; - - await Task.WhenAll(awaiter1, awaiter2, awaiter3); - Assert.True(awaiter1.IsCompleted); - Assert.True(awaiter2.IsCompleted); - Assert.True(awaiter3.IsCompleted); + foreach (var step in StepsWhichEvaluateToNullErrorProperty()) + yield return [step.step, false, step.cancel]; + foreach (var step in CancelledStepsWithErrorProperty()) + yield return [step, true, true]; + foreach (var step in FailedSteps()) + yield return [step, true, false]; } - [Fact] - public async Task GetAwaiter_WithCancellation_PropagatesCancellation() + private static IEnumerable<(ErrorStep step, bool cancel)> StepsWhichEvaluateToNullErrorProperty() { - var step = new TestStep(ct => - { - ct.ThrowIfCancellationRequested(); - return Task.CompletedTask; - }, ServiceProvider); - - var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(() => step.RunAsync(cts.Token)); - await Assert.ThrowsAsync(async () => await step); + yield return (new ErrorStep(null), false); + yield return (new ErrorStep(new StopRunnerException()), false); + yield return (new ErrorStep(new OperationCanceledException()), true); + yield return (new ErrorStep(new TaskCanceledException()), true); + yield return (new ErrorStep(new AggregateException(new OperationCanceledException())), true); + yield return (new ErrorStep(new AggregateException(new TaskCanceledException())), true); + yield return (new ErrorStep(new AggregateException(new AggregateException(new OperationCanceledException()))), true); + yield return (new ErrorStep(new AggregateException(new Exception(), new OperationCanceledException())), true); } - [Fact] - public async Task GetAwaiter_AfterSuccessfulRun_CanBeAwaitedMultipleTimes() + private static IEnumerable CancelledStepsWithErrorProperty() { - var executionCount = 0; - var step = new TestStep(_ => - { - Interlocked.Increment(ref executionCount); - return Task.CompletedTask; - }, ServiceProvider); - - await step.RunAsync(CancellationToken.None); - Assert.Equal(1, executionCount); - - await step; - await step; - await step; - - Assert.Equal(1, executionCount); + yield return new ErrorStep(new OperationCanceledException("Cancel", new Exception("Test"))); + yield return new ErrorStep(new AggregateException(new OperationCanceledException("Cancel", new Exception("Test")))); + yield return new ErrorStep(new AggregateException(new AggregateException(new OperationCanceledException("Cancel", new Exception("Test"))))); } - - [Fact] - public void ToString_IsTypeName() + private static IEnumerable FailedSteps() { - var step = new TestStep(null, ServiceProvider); - Assert.Equal(step.GetType().Name, step.ToString()); + yield return new ErrorStep(new Exception()); + yield return new ErrorStep(new AggregateException(new ArgumentException())); } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestSuite.cs similarity index 78% rename from src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestBase.cs rename to src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestSuite.cs index 758e7ac..8b025db 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestSuite.cs @@ -1,8 +1,8 @@ -using System; +using AnakinRaW.CommonUtilities.SimplePipeline.Steps; +using AnakinRaW.CommonUtilities.Testing; +using System; using System.Threading; using System.Threading.Tasks; -using AnakinRaW.CommonUtilities.SimplePipeline.Steps; -using AnakinRaW.CommonUtilities.Testing; using Xunit; namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; @@ -11,7 +11,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; /// Abstract base class for testing PipelineStep implementations. /// Subclasses MUST define expected behavior via abstract properties. /// -public abstract class PipelineStepTestBase : TestBaseWithServiceProvider +public abstract class PipelineStepTestSuite : TestBaseWithServiceProvider { /// /// Factory method to create a step for basic testing. @@ -33,11 +33,6 @@ public abstract class PipelineStepTestBase : TestBaseWithServiceProvider /// protected abstract bool StepAddsExceptionsToErrorProperty { get; } - /// - /// Defines whether StopRunnerException is added to the Error property. - /// - protected abstract bool StepAddsStopRunnerExceptionToErrorProperty { get; } - /// /// Defines whether the Step throws exceptions at all /// @@ -140,12 +135,13 @@ public async Task RunAsync_WithCancellation_ThrowsOperationCanceledException() await Task.Delay(50, TestContext.Current.CancellationToken); Assert.False(runTask.IsCompleted); // Step ignores cancellation + Assert.False(step.IsCancelled); tcs.SetResult(true); await runTask; } else { - var step2 = CreateStepWithAction(ct => + var step = CreateStepWithAction(ct => { ct.ThrowIfCancellationRequested(); return Task.CompletedTask; @@ -154,8 +150,9 @@ public async Task RunAsync_WithCancellation_ThrowsOperationCanceledException() var cts2 = new CancellationTokenSource(); cts2.Cancel(); - await Assert.ThrowsAsync(() => step2.RunAsync(cts2.Token)); - Assert.Null(step2.Error); + await Assert.ThrowsAsync(() => step.RunAsync(cts2.Token)); + Assert.Null(step.Error); + Assert.True(step.IsCancelled); } } @@ -170,10 +167,7 @@ public async Task RunAsync_StopRunnerException_IsNotAddedToErrors() var expectedType = GetExpectedExceptionType(new StopRunnerException())!; await Assert.ThrowsAsync(expectedType, () => step.RunAsync(CancellationToken.None)); - if (StepAddsStopRunnerExceptionToErrorProperty) - Assert.NotNull(step.Error); - else - Assert.Null(step.Error); + Assert.Null(step.Error); } [Fact] @@ -246,7 +240,7 @@ public async Task RunAsync_AggregateException_OriginatedFromOperationCancelled_N #endregion - #region GetAwaiter + #region GetAwaiter / ConfigureAwait [Fact] public async Task GetAwaiter_AfterCompletion_ReturnsImmediately() @@ -261,6 +255,9 @@ public async Task GetAwaiter_AfterCompletion_ReturnsImmediately() await step.RunAsync(CancellationToken.None); await step; + await step.ConfigureAwait(false); + await step.ConfigureAwait(true); + Assert.True(executed); } @@ -274,6 +271,8 @@ public async Task GetAwaiter_BeforeStart_WaitsForCompletion() }); var awaiterTask = Task.Run(async () => await step, TestContext.Current.CancellationToken); + var configuredAwaitedT = Task.Run(async () => await step.ConfigureAwait(true), TestContext.Current.CancellationToken); + var configuredAwaitedF = Task.Run(async () => await step.ConfigureAwait(false), TestContext.Current.CancellationToken); await Task.Delay(50, TestContext.Current.CancellationToken); Assert.False(awaiterTask.IsCompleted); @@ -282,26 +281,32 @@ public async Task GetAwaiter_BeforeStart_WaitsForCompletion() await Task.Delay(50, TestContext.Current.CancellationToken); Assert.False(awaiterTask.IsCompleted); + Assert.False(configuredAwaitedT.IsCompleted); + Assert.False(configuredAwaitedF.IsCompleted); tcs.SetResult(true); await runTask; await awaiterTask; + await configuredAwaitedT; + await configuredAwaitedF; + Assert.True(awaiterTask.IsCompleted); + Assert.True(configuredAwaitedT.IsCompleted); + Assert.True(configuredAwaitedF.IsCompleted); } [Fact] public async Task GetAwaiter_DuringExecution_WaitsForCompletion() { var tcs = new TaskCompletionSource(); - var step = CreateStepWithAction(async _ => - { - await tcs.Task; - }); + var step = CreateStepWithAction(async _ => { await tcs.Task; }); var runTask = step.RunAsync(CancellationToken.None); var awaiterTask = Task.Run(async () => await step, TestContext.Current.CancellationToken); + var configuredAwaitedT = Task.Run(async () => await step.ConfigureAwait(true), TestContext.Current.CancellationToken); + var configuredAwaitedF = Task.Run(async () => await step.ConfigureAwait(false), TestContext.Current.CancellationToken); await Task.Delay(50, TestContext.Current.CancellationToken); Assert.False(awaiterTask.IsCompleted); @@ -310,7 +315,12 @@ public async Task GetAwaiter_DuringExecution_WaitsForCompletion() await runTask; await awaiterTask; + await configuredAwaitedT; + await configuredAwaitedF; + Assert.True(awaiterTask.IsCompleted); + Assert.True(configuredAwaitedT.IsCompleted); + Assert.True(configuredAwaitedF.IsCompleted); } [Fact] @@ -358,6 +368,12 @@ public async Task GetAwaiter_AfterSuccessfulRun_CanBeAwaitedMultipleTimes() await step; await step; await step; + await step.ConfigureAwait(false); + await step.ConfigureAwait(false); + await step.ConfigureAwait(false); + await step.ConfigureAwait(true); + await step.ConfigureAwait(true); + await step.ConfigureAwait(true); Assert.Equal(1, executionCount); } @@ -375,6 +391,8 @@ public async Task GetAwaiter_PropagatesException() await Assert.ThrowsAsync(expectedType, () => step.RunAsync(CancellationToken.None)); await Assert.ThrowsAsync(expectedType, async () => await step); + await Assert.ThrowsAsync(expectedType, async () => await step.ConfigureAwait(false)); + await Assert.ThrowsAsync(expectedType, async () => await step.ConfigureAwait(true)); } [Fact] @@ -394,6 +412,31 @@ public async Task GetAwaiter_WithCancellation_PropagatesCancellation() await Assert.ThrowsAsync(() => step.RunAsync(cts.Token)); await Assert.ThrowsAsync(async () => await step); + await Assert.ThrowsAsync(async () => await step.ConfigureAwait(false)); + await Assert.ThrowsAsync(async () => await step.ConfigureAwait(true)); + } + + [Fact] + public void AwaiterAndAwaitableEquality() + { + ConfigureAwaitTestExtensions.AwaiterAndAwaitableEquality( + () => CreateStepWithAction(_ => Task.CompletedTask), + step => step.GetAwaiter(), + (step, ca) => step.ConfigureAwait(ca)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [InlineData(null)] + public void OnCompleted_CompletesInAnotherSynchronizationContext(bool? continueOnCapturedContext) + { + ConfigureAwaitTestExtensions.TestOnCompletedCompletesInAnotherSynchronizationContext( + continueOnCapturedContext, + () => CreateStepWithAction(_ => Task.CompletedTask), + step => step.GetAwaiter(), + (step, ca) => step.ConfigureAwait(ca), + step => step.RunAsync(CancellationToken.None)); } #endregion diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs index 5a66303..349c1b0 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/RunPipelineStepTest.cs @@ -11,14 +11,12 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class RunPipelineStepTest : PipelineStepTestBase +public class RunPipelineStepTest : PipelineStepTestSuite { protected override bool StepRespectsCancellationToken => true; protected override bool StepAddsExceptionsToErrorProperty => true; - protected override bool StepAddsStopRunnerExceptionToErrorProperty => false; - protected override PipelineStep CreateStep() { var steps = new List(); diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs b/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs index a39e73a..1c739fe 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/WaitStepTest.cs @@ -8,11 +8,10 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Test.Steps; -public class WaitStepTest : PipelineStepTestBase +public class WaitStepTest : PipelineStepTestSuite { protected override bool StepRespectsCancellationToken => false; // WaitStep ignores cancellation! protected override bool StepAddsExceptionsToErrorProperty => false; // Transforms to StopRunnerException - protected override bool StepAddsStopRunnerExceptionToErrorProperty => false; protected override Type GetExpectedExceptionType(Exception thrownException) { @@ -48,7 +47,7 @@ public void ToString_HasExpectedValue() { var runner = new AsyncStepRunner(1, ServiceProvider); var step = new WaitStep(runner, ServiceProvider); - Assert.Equal("Waiting for other steps", step.ToString()); + Assert.Equal("Waiting for other step runner", step.ToString()); } [Fact] diff --git a/src/CommonUtilities.SimplePipeline/test/StopRunnerExceptionTests.cs b/src/CommonUtilities.SimplePipeline/test/StopRunnerExceptionTests.cs new file mode 100644 index 0000000..3afe63b --- /dev/null +++ b/src/CommonUtilities.SimplePipeline/test/StopRunnerExceptionTests.cs @@ -0,0 +1,22 @@ +using AnakinRaW.CommonUtilities.Testing; +using AnakinRaW.CommonUtilities.Testing.Extensions; +using Xunit; + +namespace AnakinRaW.CommonUtilities.SimplePipeline.Test; + +public class StopRunnerExceptionTests : TestBaseWithServiceProvider +{ + [Fact] + public void Ctor_Default_DoesNotThrow() + { + var ex = new StopRunnerException(); + Assert.Exception(ex, message: "Stopping step runner."); + } + + [Fact] + public void Ctor_WithMessage_SetsMessage() + { + var ex = new StopRunnerException("Test Message"); + Assert.Exception(ex, message: "Test Message"); + } +} \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/TestData/TestStep.cs b/src/CommonUtilities.SimplePipeline/test/TestData/TestStep.cs index 61c2b8c..ffd7a0d 100644 --- a/src/CommonUtilities.SimplePipeline/test/TestData/TestStep.cs +++ b/src/CommonUtilities.SimplePipeline/test/TestData/TestStep.cs @@ -1,5 +1,6 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Progress; using AnakinRaW.CommonUtilities.SimplePipeline.Steps; +using Microsoft.Extensions.DependencyInjection; using System; using System.Threading; using System.Threading.Tasks; @@ -39,6 +40,16 @@ public static async Task CreateFailed(Exception? error, IServiceProvider } } +public class ErrorStep(Exception? exceptionToThrow) : PipelineStep(new ServiceCollection().BuildServiceProvider()) +{ + protected override Task RunCoreAsync(CancellationToken token) + { + if (exceptionToThrow is not null) + throw exceptionToThrow; + return Task.CompletedTask; + } +} + public class TestProgressStep(long size, string text, IServiceProvider serviceProvider) : TestStep(serviceProvider), IProgressStep diff --git a/src/CommonUtilities/src/AwaitExtensions.cs b/src/CommonUtilities/src/AwaitExtensions.cs index fa8472c..3bf13b9 100644 --- a/src/CommonUtilities/src/AwaitExtensions.cs +++ b/src/CommonUtilities/src/AwaitExtensions.cs @@ -25,7 +25,6 @@ public static #if !NET async #endif - Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default) { if (process == null) From feabb265f8d7255be558fde09893c92286c21ff2 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Sun, 18 Jan 2026 22:52:30 +0100 Subject: [PATCH 41/43] Thread-safe awaitable task consistency for steps All awaiter methods of IStepRunner and PipelineStep now guarantee returning the same task instance, ensuring status and exception consistency. The caching logic was replaced with an atomically set field. The Wait(TimeSpan) method now explicitly validates timeouts and documents ArgumentOutOfRangeException. New tests validate the consistent behavior of RunAsync, GetAwaiter, and ConfigureAwait. Various minor fixes and documentation improvements were made. --- .../src/IStep.cs | 7 ++ .../src/IStepRunner.cs | 11 +++ .../src/Runners/AsyncStepRunner.cs | 45 ++++++------- .../src/Steps/PipelineStep.cs | 25 +++---- .../test/Runners/StepRunnerTestSuite.cs | 67 +++++++++++++++++++ .../test/Steps/PipelineStepTestSuite.cs | 64 ++++++++++++++++++ 6 files changed, 183 insertions(+), 36 deletions(-) diff --git a/src/CommonUtilities.SimplePipeline/src/IStep.cs b/src/CommonUtilities.SimplePipeline/src/IStep.cs index a9e6ef6..3e78c9b 100644 --- a/src/CommonUtilities.SimplePipeline/src/IStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/IStep.cs @@ -37,4 +37,11 @@ public interface IStep : IDisposable /// Gets an awaiter used to await this . /// TaskAwaiter GetAwaiter(); + + /// + /// Configures an awaiter used to await this . + /// + /// + /// An object used to await this task. + ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext); } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs index 2816656..1101b31 100644 --- a/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs +++ b/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs @@ -75,6 +75,10 @@ public interface IStepRunner /// The time duration to wait. /// If expired. /// If any of the steps failed with an exception. + /// + /// is a negative number other than -1 milliseconds, which represents an infinite time-out. + /// or is greater than Int32.MaxValue. + /// void Wait(TimeSpan waitDuration); /// @@ -98,4 +102,11 @@ public interface IStepRunner /// /// A instance that can be used to await the runner's completion. TaskAwaiter GetAwaiter(); + + /// + /// Configures an awaiter used to await this . + /// + /// + /// An object used to await this task. + ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext); } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs index cc2880f..96b922a 100644 --- a/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs +++ b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs @@ -31,8 +31,7 @@ public class AsyncStepRunner : IStepRunner private readonly ConcurrentBag _executedSteps = []; private readonly ConcurrentBag _exceptions = []; private readonly TaskCompletionSource _completionSource = new(); - - private Task? _cachedAwaitableTask; + private Task? _exposedTask; /// public AggregateException? Exception => _exceptions.IsEmpty ? null : new AggregateException(_exceptions); @@ -95,10 +94,10 @@ public Task RunAsync(CancellationToken token) { if (IsRunning) throw new InvalidOperationException("The step runner is already running."); - + var task = CreateRunnerTask(token); _completionSource.TrySetResult(task); - return task; + return GetRunnerTask(); } /// @@ -107,14 +106,7 @@ public TaskAwaiter GetAwaiter() return GetRunnerTask().GetAwaiter(); } - /// - /// Configures an awaiter used to await this runner. - /// - /// - /// to attempt to marshal the continuation back to the original context captured; - /// otherwise, . - /// - /// An object used to await this runner. + /// public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) { return GetRunnerTask().ConfigureAwait(continueOnCapturedContext); @@ -122,18 +114,23 @@ public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) private Task GetRunnerTask() { + var existing = Volatile.Read(ref _exposedTask); + if (existing is not null) + return existing; + var tcsTask = _completionSource.Task; if (tcsTask is { IsCompleted: true, Status: TaskStatus.RanToCompletion }) - return tcsTask.Result; - - if (_cachedAwaitableTask is not null) - return _cachedAwaitableTask; + { + var result = tcsTask.Result; + var original = Interlocked.CompareExchange(ref _exposedTask, result, null); + return original ?? result; + } var newTask = CreateAwaitableTask(); - return Interlocked.CompareExchange(ref _cachedAwaitableTask, newTask, null) ?? newTask; + var prev = Interlocked.CompareExchange(ref _exposedTask, newTask, null); + return prev ?? newTask; } - private async Task CreateAwaitableTask() { var task = await _completionSource.Task.ConfigureAwait(false); @@ -149,11 +146,11 @@ public void Wait() /// public void Wait(TimeSpan timeout) { - var tcsTask = _completionSource.Task; - if (!tcsTask.Wait(timeout)) - throw new TimeoutException(); + var totalMilliseconds = (long)timeout.TotalMilliseconds; + if (totalMilliseconds is < -1 or > int.MaxValue) + throw new ArgumentOutOfRangeException(nameof(timeout)); - var task = tcsTask.Result; + var task = GetRunnerTask(); var completed = true; try @@ -178,7 +175,7 @@ public void Wait(TimeSpan timeout) /// /// A to observe while waiting for the next step. /// - /// A representing the asynchronous operation. + /// A representing the asynchronous operation. /// The result contains the next to be executed, or if no steps are available. /// /// @@ -234,7 +231,7 @@ private async Task CreateRunnerTask(CancellationToken token) () => RunWorkerAsync(token), CancellationToken.None, TaskCreationOptions.LongRunning, - TaskScheduler.Default).Unwrap(); + TaskScheduler.Default).Unwrap(); await Task.WhenAll(workers).ConfigureAwait(false); } } diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs index e4001b3..0e3ea81 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs @@ -13,7 +13,7 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps; public abstract class PipelineStep : DisposableObject, IStep { private readonly TaskCompletionSource _completionSource = new(); - private Task? _cachedAwaitableTask; + private Task? _exposedTask; /// /// Returns the service provider of this step. @@ -74,17 +74,12 @@ public TaskAwaiter GetAwaiter() return GetStepTask().GetAwaiter(); } - /// - /// - /// - /// - /// + /// public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) { return GetStepTask().ConfigureAwait(continueOnCapturedContext); } - /// /// Returns a string that represents the current instance. /// @@ -110,15 +105,21 @@ private async Task CreateAwaitableTask() private Task GetStepTask() { + var existing = Volatile.Read(ref _exposedTask); + if (existing is not null) + return existing; + var tcsTask = _completionSource.Task; if (tcsTask is { IsCompleted: true, Status: TaskStatus.RanToCompletion }) - return tcsTask.Result; - - if (_cachedAwaitableTask is not null) - return _cachedAwaitableTask; + { + var result = tcsTask.Result; + var original = Interlocked.CompareExchange(ref _exposedTask, result, null); + return original ?? result; + } var newTask = CreateAwaitableTask(); - return Interlocked.CompareExchange(ref _cachedAwaitableTask, newTask, null) ?? newTask; + var prev = Interlocked.CompareExchange(ref _exposedTask, newTask, null); + return prev ?? newTask; } private async Task ExecuteStepAsync(CancellationToken token) diff --git a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestSuite.cs b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestSuite.cs index e04909a..f23086f 100644 --- a/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestSuite.cs +++ b/src/CommonUtilities.SimplePipeline/test/Runners/StepRunnerTestSuite.cs @@ -1712,6 +1712,73 @@ public void OnCompleted_CompletesInAnotherSynchronizationContext(bool? continueO Assert.Equal(3, executeCount); } + [Fact] + public async Task RunAsync_GetAwaiter_ConfigureAwait_ShareSameTask() + { + var runner = CreateStepRunner(); + var stepStarted = new TaskCompletionSource(); + var canComplete = new TaskCompletionSource(); + + var step = new TestStep(async _ => + { + stepStarted.SetResult(1); + await canComplete.Task; + }, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + // Get task from RunAsync + var runTask = runner.RunAsync(CancellationToken.None); + + await stepStarted.Task; + + // All should report same IsCompleted state while running + Assert.False(runTask.IsCompleted); + Assert.False(runner.GetAwaiter().IsCompleted); + Assert.False(runner.ConfigureAwait(false).GetAwaiter().IsCompleted); + Assert.False(runner.ConfigureAwait(true).GetAwaiter().IsCompleted); + + canComplete.SetResult(1); + await runTask; + + // All should now be completed + Assert.True(runTask.IsCompleted); + Assert.True(runner.GetAwaiter().IsCompleted); + Assert.True(runner.ConfigureAwait(false).GetAwaiter().IsCompleted); + Assert.True(runner.ConfigureAwait(true).GetAwaiter().IsCompleted); + } + + [Fact] + public async Task RunAsync_GetAwaiter_ConfigureAwait_PropagateExceptionConsistently() + { + var runner = CreateStepRunner(); + var expectedException = new InvalidOperationException("Test"); + + var step = new TestStep(_ => throw expectedException, ServiceProvider); + + runner.AddStep(step); + FinishAdding(runner); + + var runTask = runner.RunAsync(CancellationToken.None); + + // All should complete without throwing (exceptions are collected in runner.Exception) + await runTask; + await runner; + await runner.ConfigureAwait(false); + await runner.ConfigureAwait(true); + + // All report same completed state + Assert.True(runTask.IsCompleted); + Assert.True(runner.GetAwaiter().IsCompleted); + Assert.True(runner.ConfigureAwait(false).GetAwaiter().IsCompleted); + Assert.True(runner.ConfigureAwait(true).GetAwaiter().IsCompleted); + + // Exception is accessible via runner.Exception + Assert.NotNull(runner.Exception); + Assert.Contains(expectedException, runner.Exception.InnerExceptions); + } + #endregion #region ExecutedSteps Tests diff --git a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestSuite.cs b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestSuite.cs index 8b025db..7ab8547 100644 --- a/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestSuite.cs +++ b/src/CommonUtilities.SimplePipeline/test/Steps/PipelineStepTestSuite.cs @@ -1,4 +1,5 @@ using AnakinRaW.CommonUtilities.SimplePipeline.Steps; +using AnakinRaW.CommonUtilities.SimplePipeline.Test.TestData; using AnakinRaW.CommonUtilities.Testing; using System; using System.Threading; @@ -439,5 +440,68 @@ public void OnCompleted_CompletesInAnotherSynchronizationContext(bool? continueO step => step.RunAsync(CancellationToken.None)); } + [Fact] + public async Task RunAsync_GetAwaiter_ConfigureAwait_ShareSameTask() + { + var stepStarted = new TaskCompletionSource(); + var canComplete = new TaskCompletionSource(); + + var step = new TestStep(async _ => + { + stepStarted.SetResult(1); + await canComplete.Task; + }, ServiceProvider); + + // Get task from RunAsync + var runTask = step.RunAsync(CancellationToken.None); + + await stepStarted.Task; + + // All should report same IsCompleted state while running + Assert.False(runTask.IsCompleted); + Assert.False(step.GetAwaiter().IsCompleted); + Assert.False(step.ConfigureAwait(false).GetAwaiter().IsCompleted); + Assert.False(step.ConfigureAwait(true).GetAwaiter().IsCompleted); + + canComplete.SetResult(1); + await runTask; + + // All should now be completed + Assert.True(runTask.IsCompleted); + Assert.True(step.GetAwaiter().IsCompleted); + Assert.True(step.ConfigureAwait(false).GetAwaiter().IsCompleted); + Assert.True(step.ConfigureAwait(true).GetAwaiter().IsCompleted); + } + + [Fact] + public async Task RunAsync_GetAwaiter_ConfigureAwait_PropagateExceptionConsistently() + { + var expectedException = new InvalidOperationException("Test"); + + var step = new TestStep(_ => throw expectedException, ServiceProvider); + + var runTask = step.RunAsync(CancellationToken.None); + + // All should throw the same exception + var ex1 = await Assert.ThrowsAsync(async () => await runTask); + var ex2 = await Assert.ThrowsAsync(async () => await step); + var ex3 = await Assert.ThrowsAsync(async () => await step.ConfigureAwait(false)); + var ex4 = await Assert.ThrowsAsync(async () => await step.ConfigureAwait(true)); + + Assert.Same(expectedException, ex1); + Assert.Same(expectedException, ex2); + Assert.Same(expectedException, ex3); + Assert.Same(expectedException, ex4); + + // All report same completed state + Assert.True(runTask.IsCompleted); + Assert.True(step.GetAwaiter().IsCompleted); + Assert.True(step.ConfigureAwait(false).GetAwaiter().IsCompleted); + Assert.True(step.ConfigureAwait(true).GetAwaiter().IsCompleted); + + // Step.Error is set + Assert.Same(expectedException, step.Error); + } + #endregion } \ No newline at end of file From 088403c22fd01990fdb2c268aadd06bd39efd1b3 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 19 Jan 2026 00:49:23 +0100 Subject: [PATCH 42/43] add tests --- .../test/Pipelines/ParallelPipelineTests.cs | 17 +++++++-- .../Pipelines/ProducerConsumerPipelineTest.cs | 7 +++- .../test/Pipelines/SequentialPipelineTests.cs | 17 +++++++-- .../StepRunnerPipelineBaseTestSuite.cs | 38 ++++++++++++++++++- .../Pipelines/StepRunnerPipelineTestBase.cs | 11 ++++-- 5 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs index b2c5cb7..8414142 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ParallelPipelineTests.cs @@ -22,9 +22,12 @@ protected override StepRunnerPipeline CreateStepRunnerPipeline(IList step return new TestParallelPipeline(ServiceProvider, steps, null, GetWorkerCount(runnerBehavior), failFast); } - protected override StepRunnerPipeline CreateTrackingStepRunnerPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null) + protected override StepRunnerPipeline CreateTrackingStepRunnerPipeline( + IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, + string? throwOnMethod = null, Func, IEnumerable>? filterErrorStepsFunc = null) { - return new TrackingParallelPipeline(ServiceProvider, steps, GetWorkerCount(runnerBehavior), failFast, callOrder, throwOnMethod); + return new TrackingParallelPipeline(ServiceProvider, steps, GetWorkerCount(runnerBehavior), failFast, callOrder, + throwOnMethod, filterErrorStepsFunc); } #region Constructor Tests @@ -74,6 +77,7 @@ private class TrackingParallelPipeline : StepRunnerPipeline { private readonly IList _steps; private readonly int _workerCount; + private readonly Func, IEnumerable>? _filterErrorStepsFunc; private readonly TrackingPipelineHelper _helper; public TrackingParallelPipeline( @@ -82,15 +86,22 @@ public TrackingParallelPipeline( int workerCount, bool failFast, List callOrder, - string? throwOnMethod) + string? throwOnMethod, + Func, IEnumerable>? filterErrorStepsFunc) : base(serviceProvider) { _steps = steps; _workerCount = workerCount; + _filterErrorStepsFunc = filterErrorStepsFunc; _helper = new TrackingPipelineHelper(callOrder, throwOnMethod); FailFast = failFast; } + protected override IEnumerable GetFailedSteps(IEnumerable steps) + { + return _filterErrorStepsFunc is null ? base.GetFailedSteps(steps) : _filterErrorStepsFunc(steps); + } + protected override IStepRunner CreateRunner() { return new AsyncStepRunner(_workerCount, ServiceProvider); diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs index 88b909c..4e88ca4 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs @@ -25,9 +25,12 @@ protected override ITrackingPipeline CreateTrackingPipeline(Func CreateTrackingPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null) + protected override StepRunnerPipelineBase CreateTrackingPipeline( + IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, + string? throwOnMethod = null, Func, IEnumerable>? filterErrorStepsFunc = null) { - return new TrackingProducerConsumerPipeline(ServiceProvider, steps.ToAsyncEnumerable(), GetWorkerCount(runnerBehavior), failFast, callOrder, throwOnMethod); + return new TrackingProducerConsumerPipeline(ServiceProvider, steps.ToAsyncEnumerable(), + GetWorkerCount(runnerBehavior), failFast, callOrder, throwOnMethod); } private ProducerConsumerPipeline CreateConsumerPipeline(IAsyncEnumerable steps, bool failFast, RunnerBehavior runnerBehavior) diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs index c7ad63c..9a315a0 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/SequentialPipelineTests.cs @@ -25,11 +25,14 @@ protected override ITrackingPipeline CreateTrackingPipeline(Func steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null) + protected override StepRunnerPipeline CreateTrackingStepRunnerPipeline( + IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, + string? throwOnMethod = null, + Func, IEnumerable>? filterErrorStepsFunc = null) { if (runnerBehavior is RunnerBehavior.Concurrent) throw new NotSupportedException("Concurrent runs are not supported"); - return new TrackingSequentialPipeline(ServiceProvider, steps, failFast, callOrder, throwOnMethod); + return new TrackingSequentialPipeline(ServiceProvider, steps, failFast, callOrder, throwOnMethod, filterErrorStepsFunc); } private SequentialPipeline CreateSequentialPipeline(IList steps, bool failFast) @@ -75,6 +78,7 @@ protected override async Task> CreateRunnerSteps(CancellationToken private class TrackingSequentialPipeline : SequentialPipeline { private readonly IList _steps; + private readonly Func, IEnumerable>? _filterErrorStepsFunc; private readonly TrackingPipelineHelper _helper; public TrackingSequentialPipeline( @@ -82,10 +86,12 @@ public TrackingSequentialPipeline( IList steps, bool failFast, List callOrder, - string? throwOnMethod) + string? throwOnMethod, + Func, IEnumerable>? filterErrorStepsFunc) : base(serviceProvider) { _steps = steps; + _filterErrorStepsFunc = filterErrorStepsFunc; _helper = new TrackingPipelineHelper(callOrder, throwOnMethod); FailFast = failFast; } @@ -95,6 +101,11 @@ protected override Task> CreateRunnerSteps(CancellationToken token) return Task.FromResult(_steps); } + protected override IEnumerable GetFailedSteps(IEnumerable steps) + { + return _filterErrorStepsFunc is null ? base.GetFailedSteps(steps) : _filterErrorStepsFunc(steps); + } + protected override void OnExecuteStarted() => _helper.OnExecuteStarted(); protected override void OnRunnerExecuted() => _helper.OnRunnerExecuted(); protected override void OnExecuteCompleted() => _helper.OnExecuteCompleted(); diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs index 254eb54..be1412d 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs @@ -28,7 +28,8 @@ protected abstract StepRunnerPipelineBase CreateTrackingPipeline( bool failFast, RunnerBehavior runnerBehavior, List callOrder, - string? throwOnMethod = null); + string? throwOnMethod = null, + Func, IEnumerable>? filterErrorStepsFunc = null); protected StepRunnerPipelineBase CreateStepRunnerPipelineBase(IList steps) { @@ -470,6 +471,41 @@ public async Task RunAsync_CancelledMidSequence_StopsOnFailFast(bool failFast, R #endregion + #region GetFailedSteps + + [Fact] + public async Task GetFailedSteps_OnlyReturnsStepsWithErrors() + { + var successStep = new TestStep(_ => Task.CompletedTask, ServiceProvider); + var failedStep = new TestStep(_ => throw new InvalidOperationException("Test error"), ServiceProvider); + var anotherSuccessStep = new TestStep(_ => Task.CompletedTask, ServiceProvider); + + var pipeline = CreateStepRunnerPipelineBase([successStep, failedStep, anotherSuccessStep], false); + + var ex = await Assert.ThrowsAsync( + () => pipeline.RunAsync(TestContext.Current.CancellationToken)); + + await Assert.Single(ex.FailedSteps); + Assert.Equal(failedStep, ex.FailedSteps.First()); + } + + [Fact] + public async Task GetFailedSteps_CanBeOverridden_CustomFilteringLogic() + { + var callOrder = new List(); + + var step1 = new TestStep(_ => throw new InvalidOperationException("Error 1"), ServiceProvider); + var step2 = new TestStep(_ => throw new ArgumentException("Error 2"), ServiceProvider); + + var pipeline = CreateTrackingPipeline([step1, step2], false, GetRandomRunBehavior(), callOrder, + filterErrorStepsFunc: steps => []); + + var ex = await Record.ExceptionAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); + Assert.Null(ex); + } + + #endregion + #region Common Usage Tests [Theory] diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs index 2da1283..caf8638 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineTestBase.cs @@ -15,11 +15,16 @@ protected override StepRunnerPipelineBase CreateStepRunnerPipelineB return CreateStepRunnerPipeline(steps, failFast, runnerBehavior); } - protected abstract StepRunnerPipeline CreateTrackingStepRunnerPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null); + protected abstract StepRunnerPipeline CreateTrackingStepRunnerPipeline( + IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null, + Func, IEnumerable>? filterErrorStepsFunc = null); - protected override StepRunnerPipelineBase CreateTrackingPipeline(IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, string? throwOnMethod = null) + protected override StepRunnerPipelineBase CreateTrackingPipeline( + IList steps, bool failFast, RunnerBehavior runnerBehavior, List callOrder, + string? throwOnMethod = null, + Func, IEnumerable>? filterErrorStepsFunc = null) { - return CreateTrackingStepRunnerPipeline(steps, failFast, runnerBehavior, callOrder, throwOnMethod); + return CreateTrackingStepRunnerPipeline(steps, failFast, runnerBehavior, callOrder, throwOnMethod, filterErrorStepsFunc); } #region CreateRunner From 8fbf933acf89f4dc3769671356f26c29dba91652 Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Mon, 19 Jan 2026 11:18:39 +0100 Subject: [PATCH 43/43] fix failling tests --- .../src/Steps/PipelineStep.cs | 2 +- .../test/Pipelines/ProducerConsumerPipelineTest.cs | 11 +++++++++-- .../test/Pipelines/StepRunnerPipelineBaseTestSuite.cs | 7 ++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs index 0e3ea81..b57de66 100644 --- a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs +++ b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs @@ -65,7 +65,7 @@ public Task RunAsync(CancellationToken token) { var task = ExecuteStepAsync(token); _completionSource.TrySetResult(task); - return task; + return GetStepTask(); } /// diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs index 4e88ca4..b56eb73 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/ProducerConsumerPipelineTest.cs @@ -30,7 +30,7 @@ protected override StepRunnerPipelineBase CreateTrac string? throwOnMethod = null, Func, IEnumerable>? filterErrorStepsFunc = null) { return new TrackingProducerConsumerPipeline(ServiceProvider, steps.ToAsyncEnumerable(), - GetWorkerCount(runnerBehavior), failFast, callOrder, throwOnMethod); + GetWorkerCount(runnerBehavior), failFast, callOrder, throwOnMethod, filterErrorStepsFunc); } private ProducerConsumerPipeline CreateConsumerPipeline(IAsyncEnumerable steps, bool failFast, RunnerBehavior runnerBehavior) @@ -697,6 +697,7 @@ private class TestProducerConsumerPipelineExposed( private class TrackingProducerConsumerPipeline : ProducerConsumerPipeline { private readonly IAsyncEnumerable _steps; + private readonly Func, IEnumerable>? _filterErrorStepsFunc; private readonly TrackingPipelineHelper _helper; public TrackingProducerConsumerPipeline( @@ -705,10 +706,12 @@ public TrackingProducerConsumerPipeline( int workerCount, bool failFast, List callOrder, - string? throwOnMethod) + string? throwOnMethod, + Func, IEnumerable>? filterErrorStepsFunc) : base(workerCount, serviceProvider) { _steps = steps; + _filterErrorStepsFunc = filterErrorStepsFunc; _helper = new TrackingPipelineHelper(callOrder, throwOnMethod); FailFast = failFast; } @@ -721,5 +724,9 @@ protected override IAsyncEnumerable BuildStepsAsync(CancellationToken tok protected override void OnExecuteStarted() => _helper.OnExecuteStarted(); protected override void OnRunnerExecuted() => _helper.OnRunnerExecuted(); protected override void OnExecuteCompleted() => _helper.OnExecuteCompleted(); + protected override IEnumerable GetFailedSteps(IEnumerable steps) + { + return _filterErrorStepsFunc is null ? base.GetFailedSteps(steps) : _filterErrorStepsFunc(steps); + } } } \ No newline at end of file diff --git a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs index be1412d..5b5405b 100644 --- a/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs +++ b/src/CommonUtilities.SimplePipeline/test/Pipelines/StepRunnerPipelineBaseTestSuite.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -485,7 +484,9 @@ public async Task GetFailedSteps_OnlyReturnsStepsWithErrors() var ex = await Assert.ThrowsAsync( () => pipeline.RunAsync(TestContext.Current.CancellationToken)); - await Assert.Single(ex.FailedSteps); +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Assert.Single(ex.FailedSteps); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed Assert.Equal(failedStep, ex.FailedSteps.First()); } @@ -498,7 +499,7 @@ public async Task GetFailedSteps_CanBeOverridden_CustomFilteringLogic() var step2 = new TestStep(_ => throw new ArgumentException("Error 2"), ServiceProvider); var pipeline = CreateTrackingPipeline([step1, step2], false, GetRandomRunBehavior(), callOrder, - filterErrorStepsFunc: steps => []); + filterErrorStepsFunc: _ => []); var ex = await Record.ExceptionAsync(async () => await pipeline.RunAsync(TestContext.Current.CancellationToken)); Assert.Null(ex);