From 7b11e8e91b357c01d6855daa85e419cae6d21350 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Thu, 10 Sep 2026 22:44:35 +0100 Subject: [PATCH 1/2] fix(build): support configured collection expressions Update capacity- and comparer-based List, HashSet, and Dictionary construction to the C# 15 collection-expression syntax required by SST2106. Refresh centrally managed dependency versions and align the affected test setup code while preserving comparer and capacity semantics. --- src/Directory.Packages.props | 50 +++++++++---------- .../SignalOperatorMixins.CollectionSignals.cs | 2 +- .../SignalOperatorMixins.StatefulSignals.cs | 6 +-- ...alOperatorParityMixins.AggregateHelpers.cs | 2 +- ...OperatorParityMixins.AwaitableTerminals.cs | 2 +- .../Operators/ToDictionaryAsync.cs | 2 +- .../Advanced/DistinctByCountAggregator.cs | 2 +- .../Advanced/DistinctByLongCountAggregator.cs | 2 +- .../Advanced/DistinctByWitness.cs | 2 +- .../AsyncBridgeGeneratorContractTests.cs | 4 +- .../SignalFactoriesTests.cs | 3 +- 11 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 5cceac0f..00373332 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,26 +5,26 @@ true - 3.46.0 + 4.0.5 12.1.2 - + - + - + - + @@ -34,41 +34,41 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + - + - - + + diff --git a/src/Primitives.Shared/SignalOperatorMixins.CollectionSignals.cs b/src/Primitives.Shared/SignalOperatorMixins.CollectionSignals.cs index 301d2546..9f9fd21d 100644 --- a/src/Primitives.Shared/SignalOperatorMixins.CollectionSignals.cs +++ b/src/Primitives.Shared/SignalOperatorMixins.CollectionSignals.cs @@ -79,7 +79,7 @@ public IDisposable Subscribe(IObserver> observer) { ArgumentExceptionHelper.ThrowIfNull(observer); - List values = new(_range.Count); + List values = [with(capacity: _range.Count)]; for (var i = 0; i < _range.Count; i++) { values.Add(_range.Start + i); diff --git a/src/Primitives.Shared/SignalOperatorMixins.StatefulSignals.cs b/src/Primitives.Shared/SignalOperatorMixins.StatefulSignals.cs index 48b4530a..8a2041a1 100644 --- a/src/Primitives.Shared/SignalOperatorMixins.StatefulSignals.cs +++ b/src/Primitives.Shared/SignalOperatorMixins.StatefulSignals.cs @@ -330,12 +330,12 @@ private HashSet CreateSeen() => #if NET8_0_OR_GREATER (_source is RangeSignal range ? range.Count : 0) switch { - var capacity when capacity > 0 => new(capacity, _comparer), + var capacity when capacity > 0 => [with(capacity, _comparer)], _ when _comparer is null => [], - _ => new(_comparer), + _ => [with(_comparer)], }; #else - new(_comparer); + [with(_comparer)]; #endif } diff --git a/src/Primitives.Shared/SignalOperatorParityMixins.AggregateHelpers.cs b/src/Primitives.Shared/SignalOperatorParityMixins.AggregateHelpers.cs index 15bf3f2d..09c0e5c5 100644 --- a/src/Primitives.Shared/SignalOperatorParityMixins.AggregateHelpers.cs +++ b/src/Primitives.Shared/SignalOperatorParityMixins.AggregateHelpers.cs @@ -104,7 +104,7 @@ private static int CountDistinctRange( Func keySelector, IEqualityComparer? comparer) { - HashSet seen = comparer is null ? [] : new(comparer); + HashSet seen = comparer is null ? [] : [with(comparer)]; var typedSelector = (Func)(object)keySelector; for (var i = 0; i < range.Count; i++) { diff --git a/src/Primitives.Shared/SignalOperatorParityMixins.AwaitableTerminals.cs b/src/Primitives.Shared/SignalOperatorParityMixins.AwaitableTerminals.cs index e992ddea..a7cde36d 100644 --- a/src/Primitives.Shared/SignalOperatorParityMixins.AwaitableTerminals.cs +++ b/src/Primitives.Shared/SignalOperatorParityMixins.AwaitableTerminals.cs @@ -278,7 +278,7 @@ public Task> CollectListAsync() if (source is RangeSignal range && typeof(T) == typeof(int)) { - List integers = new(range.Count); + List integers = [with(capacity: range.Count)]; for (var i = 0; i < range.Count; i++) { integers.Add(range.Start + i); diff --git a/src/ReactiveUI.Primitives.Async.Core/Operators/ToDictionaryAsync.cs b/src/ReactiveUI.Primitives.Async.Core/Operators/ToDictionaryAsync.cs index 9d811001..fb7f8918 100644 --- a/src/ReactiveUI.Primitives.Async.Core/Operators/ToDictionaryAsync.cs +++ b/src/ReactiveUI.Primitives.Async.Core/Operators/ToDictionaryAsync.cs @@ -148,7 +148,7 @@ private sealed class ToDictionaryTaskWitness( where TKey : notnull { /// The dictionary that accumulates key-value pairs from the source sequence. - private readonly Dictionary _map = comparer is null ? new() : new(comparer); + private readonly Dictionary _map = comparer is null ? [] : [with(comparer)]; /// protected override ValueTask OnNextAsyncCore(TSource value, CancellationToken cancellationToken) diff --git a/src/ReactiveUI.Primitives.Core/Advanced/DistinctByCountAggregator.cs b/src/ReactiveUI.Primitives.Core/Advanced/DistinctByCountAggregator.cs index bc79d070..ec35c879 100644 --- a/src/ReactiveUI.Primitives.Core/Advanced/DistinctByCountAggregator.cs +++ b/src/ReactiveUI.Primitives.Core/Advanced/DistinctByCountAggregator.cs @@ -21,7 +21,7 @@ public readonly record struct /// The key selector. /// The key comparer, or for the default comparer. public DistinctByCountAggregator(Func keySelector, IEqualityComparer? comparer) - : this(keySelector, comparer is null ? [] : new(comparer), 0) + : this(keySelector, comparer is null ? [] : [with(comparer)], 0) { } diff --git a/src/ReactiveUI.Primitives.Core/Advanced/DistinctByLongCountAggregator.cs b/src/ReactiveUI.Primitives.Core/Advanced/DistinctByLongCountAggregator.cs index 577fdc33..2dd240f2 100644 --- a/src/ReactiveUI.Primitives.Core/Advanced/DistinctByLongCountAggregator.cs +++ b/src/ReactiveUI.Primitives.Core/Advanced/DistinctByLongCountAggregator.cs @@ -21,7 +21,7 @@ public readonly record struct /// The key selector. /// The key comparer, or for the default comparer. public DistinctByLongCountAggregator(Func keySelector, IEqualityComparer? comparer) - : this(keySelector, comparer is null ? [] : new(comparer), 0L) + : this(keySelector, comparer is null ? [] : [with(comparer)], 0L) { } diff --git a/src/ReactiveUI.Primitives.Core/Advanced/DistinctByWitness.cs b/src/ReactiveUI.Primitives.Core/Advanced/DistinctByWitness.cs index b8cec493..62f8ab8b 100644 --- a/src/ReactiveUI.Primitives.Core/Advanced/DistinctByWitness.cs +++ b/src/ReactiveUI.Primitives.Core/Advanced/DistinctByWitness.cs @@ -35,7 +35,7 @@ public DistinctByWitness(IObserver observer, Func keySelector, IEqua { _observer = observer; _keySelector = keySelector; - _seen = comparer is null ? [] : new(comparer); + _seen = comparer is null ? [] : [with(comparer)]; } /// diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncBridgeGeneratorContractTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncBridgeGeneratorContractTests.cs index 35d893c5..14dd5675 100644 --- a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncBridgeGeneratorContractTests.cs +++ b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncBridgeGeneratorContractTests.cs @@ -253,7 +253,7 @@ private static List CreateReferences(bool includeAsyncReferen { // Signal and StateSignal<> are always referenced on top of the platform assemblies. const int SignalReferenceCount = 2; - Dictionary platformAssemblies = new(StringComparer.OrdinalIgnoreCase); + Dictionary platformAssemblies = [with(StringComparer.OrdinalIgnoreCase)]; foreach (var path in AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!.ToString()!.Split(Path.PathSeparator)) { var name = Path.GetFileName(path); @@ -263,7 +263,7 @@ private static List CreateReferences(bool includeAsyncReferen } } - List references = new(PlatformReferenceNames.Length + SignalReferenceCount); + List references = [with(capacity: PlatformReferenceNames.Length + SignalReferenceCount)]; foreach (var name in PlatformReferenceNames) { if (platformAssemblies.TryGetValue(name, out var path)) diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalFactoriesTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalFactoriesTests.cs index 916f98cc..9ab1e57d 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/SignalFactoriesTests.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/SignalFactoriesTests.cs @@ -527,7 +527,8 @@ public async Task RxFactoryAliasesRepeatGenerateUsingIfAndCase() chooseThen = false; _ = conditionalSource.Subscribe(conditional.Add); - Dictionary> cases = new(StringComparer.Ordinal) { ["one"] = Signal.Emit(One) }; + Dictionary> cases = [with(StringComparer.Ordinal)]; + cases["one"] = Signal.Emit(One); _ = Signal.Case(static () => "one", cases, Signal.Emit(Two)).Subscribe(selectedCase.Add); _ = Signal.Case(static () => "missing", cases, Signal.Emit(Two)).Subscribe(defaultCase.Add); _ = Signal.Using( From 7dc61c5f9e55a64ef2d9b5f1af1822b6f8913ec3 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Fri, 11 Sep 2026 00:28:31 +0100 Subject: [PATCH 2/2] test(ci): use harness cancellation for async enumeration Replace the scheduler-sensitive five-second completion wait in the async-enumerable coverage test with TUnit's standard cancellation token and a 30-second per-test timeout.\n\nThis preserves the asynchronous enumeration path while preventing Windows multi-target runner contention from producing false failures. --- .../SignalOperatorMixinsTests.Deterministic.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Deterministic.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Deterministic.cs index c523fbfd..bd9261b9 100644 --- a/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Deterministic.cs +++ b/src/tests/ReactiveUI.Primitives.Tests/SignalOperatorMixinsTests.Deterministic.cs @@ -165,11 +165,13 @@ public async Task RemainingOperatorFactoryAndObserverFailureBranchesAreDetermini } /// Verifies optimized coordinator and async enumerable branches cover remaining gaps. + /// The test cancellation token. /// A task representing the asynchronous test. [Test] - public async Task OptimizedCoordinatorAndAsyncEnumerableBranchesCoverRemainingGaps() + [Timeout(30_000)] + public async Task OptimizedCoordinatorAndAsyncEnumerableBranchesCoverRemainingGaps(CancellationToken token) { - await VerifyAsyncEnumerableShiftAndExpireAsync().ConfigureAwait(false); + await VerifyAsyncEnumerableShiftAndExpireAsync(token).ConfigureAwait(false); await VerifyRaceSyncLatestAndSwitchBranches(); await VerifyProbeBranches(); await VerifyCalmAppendAndForkJoinBranches(); @@ -462,8 +464,9 @@ private static async Task VerifyFlatMapTerminalAndErrorBranches() } /// Verifies async enumerable subscription, shift timing, and expire timeout branches. + /// The test cancellation token. /// A task representing the asynchronous verification. - private static async Task VerifyAsyncEnumerableShiftAndExpireAsync() + private static async Task VerifyAsyncEnumerableShiftAndExpireAsync(CancellationToken token) { _ = Assert.Throws(static () => Signal.FromAsyncEnumerable(AsyncValues(One)).Subscribe(null!)); List asyncValues = []; @@ -473,7 +476,7 @@ private static async Task VerifyAsyncEnumerableShiftAndExpireAsync() asyncValues.Add, ex => asyncCompleted.TrySetException(ex), () => asyncCompleted.TrySetResult(null)); - await asyncCompleted.Task.WaitAsync(TimeSpan.FromSeconds(Five)).ConfigureAwait(false); + await asyncCompleted.Task.WaitAsync(token).ConfigureAwait(false); int[] expectedAsyncValues = [0, One, Two]; await Assert.That(asyncValues.SequenceEqual(expectedAsyncValues)).IsTrue(); var exact = await Signal.FromAsyncEnumerable(AsyncValues(Sixteen)).CollectArrayAsync().ConfigureAwait(false);