From 8ead2da385dd09d4e1e1d39d29cf9387a97d80ba Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 16:01:48 +0000
Subject: [PATCH 01/43] Initial plan
From be7137b9d058e195371c3d8f64cd7e0a053fb3cf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 16:18:36 +0000
Subject: [PATCH 02/43] feat: create PowerKit source-only NuGet package with
shared BCL-only utility extensions
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/43c1b0cc-139f-46f8-9717-63e43e341fc1
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
.gitignore | 12 ++++
Directory.Build.props | 20 ++++++
License.txt | 21 ++++++
NuGet.config | 10 +++
PowerKit.slnx | 3 +
PowerKit/PowerKit.csproj | 34 +++++++++
PowerKit/PowerKit.props | 5 ++
PowerKit/Utils/Disposable.cs | 23 ++++++
.../Extensions/AsyncCollectionExtensions.cs | 53 ++++++++++++++
.../Utils/Extensions/CollectionExtensions.cs | 71 +++++++++++++++++++
.../Utils/Extensions/ExceptionExtensions.cs | 44 ++++++++++++
.../Utils/Extensions/GenericExtensions.cs | 21 ++++++
PowerKit/Utils/Extensions/PathExtensions.cs | 47 ++++++++++++
PowerKit/Utils/Extensions/StreamExtensions.cs | 67 +++++++++++++++++
PowerKit/Utils/Extensions/StringExtensions.cs | 54 ++++++++++++++
.../Utils/Extensions/TextReaderExtensions.cs | 24 +++++++
.../Utils/Extensions/TimeSpanExtensions.cs | 20 ++++++
global.json | 6 ++
18 files changed, 535 insertions(+)
create mode 100644 .gitignore
create mode 100644 Directory.Build.props
create mode 100644 License.txt
create mode 100644 NuGet.config
create mode 100644 PowerKit.slnx
create mode 100644 PowerKit/PowerKit.csproj
create mode 100644 PowerKit/PowerKit.props
create mode 100644 PowerKit/Utils/Disposable.cs
create mode 100644 PowerKit/Utils/Extensions/AsyncCollectionExtensions.cs
create mode 100644 PowerKit/Utils/Extensions/CollectionExtensions.cs
create mode 100644 PowerKit/Utils/Extensions/ExceptionExtensions.cs
create mode 100644 PowerKit/Utils/Extensions/GenericExtensions.cs
create mode 100644 PowerKit/Utils/Extensions/PathExtensions.cs
create mode 100644 PowerKit/Utils/Extensions/StreamExtensions.cs
create mode 100644 PowerKit/Utils/Extensions/StringExtensions.cs
create mode 100644 PowerKit/Utils/Extensions/TextReaderExtensions.cs
create mode 100644 PowerKit/Utils/Extensions/TimeSpanExtensions.cs
create mode 100644 global.json
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..20d4f31
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# User-specific files
+.vs/
+.idea/
+*.suo
+*.user
+
+# Build results
+bin/
+obj/
+
+# Test results
+TestResults/
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..50a8b42
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,20 @@
+
+
+ 0.0.0-dev
+ Tyrrrz
+ Copyright (C) Oleksii Holub
+ preview
+ enable
+ true
+ false
+
+
+
+ $(Company)
+ Collection of shared utility extensions for Tyrrrz projects.
+ utils extensions utilities source
+ https://github.com/Tyrrrz/PowerKit
+ https://github.com/Tyrrrz/PowerKit/releases
+ MIT
+
+
diff --git a/License.txt b/License.txt
new file mode 100644
index 0000000..321d06a
--- /dev/null
+++ b/License.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023-2026 Oleksii Holub
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/NuGet.config b/NuGet.config
new file mode 100644
index 0000000..5a40e48
--- /dev/null
+++ b/NuGet.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/PowerKit.slnx b/PowerKit.slnx
new file mode 100644
index 0000000..59e4068
--- /dev/null
+++ b/PowerKit.slnx
@@ -0,0 +1,3 @@
+
+
+
diff --git a/PowerKit/PowerKit.csproj b/PowerKit/PowerKit.csproj
new file mode 100644
index 0000000..7a27e2e
--- /dev/null
+++ b/PowerKit/PowerKit.csproj
@@ -0,0 +1,34 @@
+
+
+
+
+ net9.0
+ true
+
+
+
+ true
+ false
+ true
+
+
+
+
+
+
+
+
diff --git a/PowerKit/PowerKit.props b/PowerKit/PowerKit.props
new file mode 100644
index 0000000..f553c39
--- /dev/null
+++ b/PowerKit/PowerKit.props
@@ -0,0 +1,5 @@
+
+
+ preview
+
+
diff --git a/PowerKit/Utils/Disposable.cs b/PowerKit/Utils/Disposable.cs
new file mode 100644
index 0000000..c1aa324
--- /dev/null
+++ b/PowerKit/Utils/Disposable.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+
+namespace PowerKit.Utils;
+
+internal partial class Disposable(Action dispose) : IDisposable
+{
+ public void Dispose() => dispose();
+}
+
+internal partial class Disposable
+{
+ public static IDisposable Null { get; } = Create(() => { });
+
+ public static IDisposable Create(Action dispose) => new Disposable(dispose);
+
+ public static IDisposable Merge(params IEnumerable disposables) =>
+ Create(() =>
+ {
+ foreach (var disposable in disposables)
+ disposable.Dispose();
+ });
+}
diff --git a/PowerKit/Utils/Extensions/AsyncCollectionExtensions.cs b/PowerKit/Utils/Extensions/AsyncCollectionExtensions.cs
new file mode 100644
index 0000000..3806161
--- /dev/null
+++ b/PowerKit/Utils/Extensions/AsyncCollectionExtensions.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class AsyncCollectionExtensions
+{
+ extension(IAsyncEnumerable source)
+ {
+ public async IAsyncEnumerable TakeAsync(
+ int count,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default
+ )
+ {
+ var currentCount = 0;
+
+ await foreach (var item in source.WithCancellation(cancellationToken))
+ {
+ if (currentCount >= count)
+ yield break;
+
+ yield return item;
+ currentCount++;
+ }
+ }
+
+ public async IAsyncEnumerable SelectManyAsync(
+ System.Func> transform,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default
+ )
+ {
+ await foreach (var item in source.WithCancellation(cancellationToken))
+ foreach (var result in transform(item))
+ yield return result;
+ }
+
+ public async ValueTask> ToListAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ var list = new List();
+
+ await foreach (var item in source.WithCancellation(cancellationToken))
+ list.Add(item);
+
+ return list;
+ }
+
+ public ValueTaskAwaiter> GetAwaiter() => source.ToListAsync().GetAwaiter();
+ }
+}
diff --git a/PowerKit/Utils/Extensions/CollectionExtensions.cs b/PowerKit/Utils/Extensions/CollectionExtensions.cs
new file mode 100644
index 0000000..99914ea
--- /dev/null
+++ b/PowerKit/Utils/Extensions/CollectionExtensions.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class CollectionExtensions
+{
+ extension(T obj)
+ {
+ public IEnumerable ToSingletonEnumerable()
+ {
+ yield return obj;
+ }
+ }
+
+ extension(IEnumerable source)
+ where T : class
+ {
+ public IEnumerable WhereNotNull()
+ {
+ foreach (var item in source)
+ {
+ if (item is not null)
+ yield return item;
+ }
+ }
+ }
+
+ extension(IEnumerable source)
+ where T : struct
+ {
+ public IEnumerable WhereNotNull()
+ {
+ foreach (var item in source)
+ {
+ if (item is not null)
+ yield return item.Value;
+ }
+ }
+ }
+
+ extension(IEnumerable source)
+ {
+ public IEnumerable WhereNotNullOrWhiteSpace()
+ {
+ foreach (var item in source)
+ {
+ if (!string.IsNullOrWhiteSpace(item))
+ yield return item;
+ }
+ }
+ }
+
+ extension(IEnumerable source)
+ where T : struct
+ {
+ public T? FirstOrNull()
+ {
+ foreach (var item in source)
+ return item;
+
+ return null;
+ }
+
+ public T? ElementAtOrNull(int index)
+ {
+ var list = source as IReadOnlyList ?? source.ToArray();
+ return index < list.Count ? list[index] : null;
+ }
+ }
+}
diff --git a/PowerKit/Utils/Extensions/ExceptionExtensions.cs b/PowerKit/Utils/Extensions/ExceptionExtensions.cs
new file mode 100644
index 0000000..5a29cb7
--- /dev/null
+++ b/PowerKit/Utils/Extensions/ExceptionExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class ExceptionExtensions
+{
+ extension(AggregateException exception)
+ {
+ public Exception? TryGetSingle()
+ {
+ var exceptions = exception.Flatten().InnerExceptions;
+ return exceptions.Count == 1 ? exceptions.Single() : null;
+ }
+ }
+
+ extension(Exception exception)
+ {
+ private void PopulateDescendants(ICollection result)
+ {
+ if (exception is AggregateException aggregateException)
+ {
+ foreach (var innerException in aggregateException.InnerExceptions)
+ {
+ result.Add(innerException);
+ innerException.PopulateDescendants(result);
+ }
+ }
+ else if (exception.InnerException is not null)
+ {
+ result.Add(exception.InnerException);
+ exception.InnerException.PopulateDescendants(result);
+ }
+ }
+
+ public IReadOnlyList GetSelfAndDescendants()
+ {
+ var result = new List { exception };
+ exception.PopulateDescendants(result);
+ return result;
+ }
+ }
+}
diff --git a/PowerKit/Utils/Extensions/GenericExtensions.cs b/PowerKit/Utils/Extensions/GenericExtensions.cs
new file mode 100644
index 0000000..73c120d
--- /dev/null
+++ b/PowerKit/Utils/Extensions/GenericExtensions.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class GenericExtensions
+{
+ extension(TIn input)
+ {
+ public TOut Pipe(Func transform) => transform(input);
+ }
+
+ extension(T value)
+ where T : struct
+ {
+ public T? NullIf(Func predicate) => !predicate(value) ? value : null;
+
+ public T? NullIfDefault() =>
+ value.NullIf(v => EqualityComparer.Default.Equals(v, default));
+ }
+}
diff --git a/PowerKit/Utils/Extensions/PathExtensions.cs b/PowerKit/Utils/Extensions/PathExtensions.cs
new file mode 100644
index 0000000..d01c27d
--- /dev/null
+++ b/PowerKit/Utils/Extensions/PathExtensions.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class PathExtensions
+{
+ // This is a union of invalid characters from Windows (NTFS/FAT32), Linux (ext4/XFS), and macOS (HFS+/APFS).
+ // We use this instead of Path.GetInvalidFileNameChars() because that only returns OS-specific characters,
+ // not filesystem-specific characters. It's possible to use, for example, an NTFS drive on Linux,
+ // which would make some additional characters invalid that are otherwise valid on Linux.
+ private static readonly HashSet InvalidFileNameChars =
+ [
+ '\0', // Null character - invalid on all filesystems
+ '/', // Path separator on Unix and Windows
+ '\\', // Path separator on Windows
+ ':', // Reserved on Windows (drive letters, NTFS streams)
+ '*', // Wildcard on Windows
+ '?', // Wildcard on Windows
+ '"', // Reserved on Windows
+ '<', // Redirection on Windows
+ '>', // Redirection on Windows
+ '|', // Pipe on Windows
+ ];
+
+ extension(Path)
+ {
+ public static string EscapeFileName(string path)
+ {
+ var buffer = new StringBuilder(path.Length);
+
+ foreach (var c in path)
+ buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
+
+ // File names cannot end with a dot on Windows
+ if (OperatingSystem.IsWindows())
+ {
+ while (buffer.Length > 0 && buffer[^1] == '.')
+ buffer.Remove(buffer.Length - 1, 1);
+ }
+
+ return buffer.ToString();
+ }
+ }
+}
diff --git a/PowerKit/Utils/Extensions/StreamExtensions.cs b/PowerKit/Utils/Extensions/StreamExtensions.cs
new file mode 100644
index 0000000..b044769
--- /dev/null
+++ b/PowerKit/Utils/Extensions/StreamExtensions.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Buffers;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class StreamExtensions
+{
+ extension(Stream source)
+ {
+ public async Task CopyToAsync(
+ Stream destination,
+ bool autoFlush,
+ CancellationToken cancellationToken = default
+ )
+ {
+ using var buffer = MemoryPool.Shared.Rent(81920);
+
+ while (true)
+ {
+ var bytesRead = await source
+ .ReadAsync(buffer.Memory, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (bytesRead <= 0)
+ break;
+
+ await destination
+ .WriteAsync(buffer.Memory[..bytesRead], cancellationToken)
+ .ConfigureAwait(false);
+
+ if (autoFlush)
+ await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ public async ValueTask CopyToAsync(
+ Stream destination,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ using var buffer = MemoryPool.Shared.Rent(81920);
+
+ var totalBytesRead = 0L;
+
+ while (true)
+ {
+ var bytesRead = await source
+ .ReadAsync(buffer.Memory, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (bytesRead <= 0)
+ break;
+
+ await destination
+ .WriteAsync(buffer.Memory[..bytesRead], cancellationToken)
+ .ConfigureAwait(false);
+
+ totalBytesRead += bytesRead;
+ progress?.Report(1.0 * totalBytesRead / source.Length);
+ }
+ }
+ }
+}
diff --git a/PowerKit/Utils/Extensions/StringExtensions.cs b/PowerKit/Utils/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..d17e890
--- /dev/null
+++ b/PowerKit/Utils/Extensions/StringExtensions.cs
@@ -0,0 +1,54 @@
+using System.Text;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class StringExtensions
+{
+ extension(string str)
+ {
+ public string? NullIfWhiteSpace() => !string.IsNullOrWhiteSpace(str) ? str : null;
+
+ public string SubstringUntil(
+ string sub,
+ System.StringComparison comparison = System.StringComparison.Ordinal
+ ) =>
+ str.IndexOf(sub, comparison) switch
+ {
+ >= 0 and var index => str[..index],
+ _ => str,
+ };
+
+ public string SubstringAfter(
+ string sub,
+ System.StringComparison comparison = System.StringComparison.Ordinal
+ ) =>
+ str.IndexOf(sub, comparison) switch
+ {
+ >= 0 and var index => str[(index + sub.Length)..],
+ _ => "",
+ };
+
+ public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str;
+
+ public string ToSpaceSeparatedWords()
+ {
+ var builder = new StringBuilder(str.Length * 2);
+
+ foreach (var c in str)
+ {
+ if (char.IsUpper(c) && builder.Length > 0)
+ builder.Append(' ');
+
+ builder.Append(c);
+ }
+
+ return builder.ToString();
+ }
+ }
+
+ extension(StringBuilder builder)
+ {
+ public StringBuilder AppendIfNotEmpty(char value) =>
+ builder.Length > 0 ? builder.Append(value) : builder;
+ }
+}
diff --git a/PowerKit/Utils/Extensions/TextReaderExtensions.cs b/PowerKit/Utils/Extensions/TextReaderExtensions.cs
new file mode 100644
index 0000000..68363ea
--- /dev/null
+++ b/PowerKit/Utils/Extensions/TextReaderExtensions.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class TextReaderExtensions
+{
+ extension(TextReader reader)
+ {
+ public async IAsyncEnumerable ReadLinesAsync(
+ [EnumeratorCancellation] CancellationToken cancellationToken = default
+ )
+ {
+ while (
+ await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line
+ )
+ {
+ yield return line;
+ }
+ }
+ }
+}
diff --git a/PowerKit/Utils/Extensions/TimeSpanExtensions.cs b/PowerKit/Utils/Extensions/TimeSpanExtensions.cs
new file mode 100644
index 0000000..ea0a025
--- /dev/null
+++ b/PowerKit/Utils/Extensions/TimeSpanExtensions.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace PowerKit.Utils.Extensions;
+
+internal static class TimeSpanExtensions
+{
+ extension(TimeSpan value)
+ {
+ public TimeSpan Clamp(TimeSpan min, TimeSpan max)
+ {
+ if (value < min)
+ return min;
+
+ if (value > max)
+ return max;
+
+ return value;
+ }
+ }
+}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..512142d
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "latestFeature"
+ }
+}
From ad3489c389d5f9f80743a0619d6504c482b1d6e9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 16:25:39 +0000
Subject: [PATCH 03/43] refactor: remove Utils namespace/dir, move files one
level up
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/56664dc8-6a19-453c-9c1a-fa9b6aa2d4b8
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/{Utils => }/Disposable.cs | 2 +-
PowerKit/{Utils => }/Extensions/AsyncCollectionExtensions.cs | 2 +-
PowerKit/{Utils => }/Extensions/CollectionExtensions.cs | 2 +-
PowerKit/{Utils => }/Extensions/ExceptionExtensions.cs | 2 +-
PowerKit/{Utils => }/Extensions/GenericExtensions.cs | 2 +-
PowerKit/{Utils => }/Extensions/PathExtensions.cs | 2 +-
PowerKit/{Utils => }/Extensions/StreamExtensions.cs | 2 +-
PowerKit/{Utils => }/Extensions/StringExtensions.cs | 2 +-
PowerKit/{Utils => }/Extensions/TextReaderExtensions.cs | 2 +-
PowerKit/{Utils => }/Extensions/TimeSpanExtensions.cs | 2 +-
10 files changed, 10 insertions(+), 10 deletions(-)
rename PowerKit/{Utils => }/Disposable.cs (95%)
rename PowerKit/{Utils => }/Extensions/AsyncCollectionExtensions.cs (97%)
rename PowerKit/{Utils => }/Extensions/CollectionExtensions.cs (97%)
rename PowerKit/{Utils => }/Extensions/ExceptionExtensions.cs (97%)
rename PowerKit/{Utils => }/Extensions/GenericExtensions.cs (92%)
rename PowerKit/{Utils => }/Extensions/PathExtensions.cs (97%)
rename PowerKit/{Utils => }/Extensions/StreamExtensions.cs (98%)
rename PowerKit/{Utils => }/Extensions/StringExtensions.cs (97%)
rename PowerKit/{Utils => }/Extensions/TextReaderExtensions.cs (93%)
rename PowerKit/{Utils => }/Extensions/TimeSpanExtensions.cs (89%)
diff --git a/PowerKit/Utils/Disposable.cs b/PowerKit/Disposable.cs
similarity index 95%
rename from PowerKit/Utils/Disposable.cs
rename to PowerKit/Disposable.cs
index c1aa324..c79eed0 100644
--- a/PowerKit/Utils/Disposable.cs
+++ b/PowerKit/Disposable.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
-namespace PowerKit.Utils;
+namespace PowerKit;
internal partial class Disposable(Action dispose) : IDisposable
{
diff --git a/PowerKit/Utils/Extensions/AsyncCollectionExtensions.cs b/PowerKit/Extensions/AsyncCollectionExtensions.cs
similarity index 97%
rename from PowerKit/Utils/Extensions/AsyncCollectionExtensions.cs
rename to PowerKit/Extensions/AsyncCollectionExtensions.cs
index 3806161..86ed232 100644
--- a/PowerKit/Utils/Extensions/AsyncCollectionExtensions.cs
+++ b/PowerKit/Extensions/AsyncCollectionExtensions.cs
@@ -3,7 +3,7 @@
using System.Threading;
using System.Threading.Tasks;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class AsyncCollectionExtensions
{
diff --git a/PowerKit/Utils/Extensions/CollectionExtensions.cs b/PowerKit/Extensions/CollectionExtensions.cs
similarity index 97%
rename from PowerKit/Utils/Extensions/CollectionExtensions.cs
rename to PowerKit/Extensions/CollectionExtensions.cs
index 99914ea..544bd32 100644
--- a/PowerKit/Utils/Extensions/CollectionExtensions.cs
+++ b/PowerKit/Extensions/CollectionExtensions.cs
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class CollectionExtensions
{
diff --git a/PowerKit/Utils/Extensions/ExceptionExtensions.cs b/PowerKit/Extensions/ExceptionExtensions.cs
similarity index 97%
rename from PowerKit/Utils/Extensions/ExceptionExtensions.cs
rename to PowerKit/Extensions/ExceptionExtensions.cs
index 5a29cb7..0b13f52 100644
--- a/PowerKit/Utils/Extensions/ExceptionExtensions.cs
+++ b/PowerKit/Extensions/ExceptionExtensions.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class ExceptionExtensions
{
diff --git a/PowerKit/Utils/Extensions/GenericExtensions.cs b/PowerKit/Extensions/GenericExtensions.cs
similarity index 92%
rename from PowerKit/Utils/Extensions/GenericExtensions.cs
rename to PowerKit/Extensions/GenericExtensions.cs
index 73c120d..e69e82c 100644
--- a/PowerKit/Utils/Extensions/GenericExtensions.cs
+++ b/PowerKit/Extensions/GenericExtensions.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class GenericExtensions
{
diff --git a/PowerKit/Utils/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
similarity index 97%
rename from PowerKit/Utils/Extensions/PathExtensions.cs
rename to PowerKit/Extensions/PathExtensions.cs
index d01c27d..2a83ee4 100644
--- a/PowerKit/Utils/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -3,7 +3,7 @@
using System.IO;
using System.Text;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class PathExtensions
{
diff --git a/PowerKit/Utils/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs
similarity index 98%
rename from PowerKit/Utils/Extensions/StreamExtensions.cs
rename to PowerKit/Extensions/StreamExtensions.cs
index b044769..2e5a37b 100644
--- a/PowerKit/Utils/Extensions/StreamExtensions.cs
+++ b/PowerKit/Extensions/StreamExtensions.cs
@@ -4,7 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class StreamExtensions
{
diff --git a/PowerKit/Utils/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs
similarity index 97%
rename from PowerKit/Utils/Extensions/StringExtensions.cs
rename to PowerKit/Extensions/StringExtensions.cs
index d17e890..9e7790b 100644
--- a/PowerKit/Utils/Extensions/StringExtensions.cs
+++ b/PowerKit/Extensions/StringExtensions.cs
@@ -1,6 +1,6 @@
using System.Text;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class StringExtensions
{
diff --git a/PowerKit/Utils/Extensions/TextReaderExtensions.cs b/PowerKit/Extensions/TextReaderExtensions.cs
similarity index 93%
rename from PowerKit/Utils/Extensions/TextReaderExtensions.cs
rename to PowerKit/Extensions/TextReaderExtensions.cs
index 68363ea..08e1323 100644
--- a/PowerKit/Utils/Extensions/TextReaderExtensions.cs
+++ b/PowerKit/Extensions/TextReaderExtensions.cs
@@ -3,7 +3,7 @@
using System.Runtime.CompilerServices;
using System.Threading;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class TextReaderExtensions
{
diff --git a/PowerKit/Utils/Extensions/TimeSpanExtensions.cs b/PowerKit/Extensions/TimeSpanExtensions.cs
similarity index 89%
rename from PowerKit/Utils/Extensions/TimeSpanExtensions.cs
rename to PowerKit/Extensions/TimeSpanExtensions.cs
index ea0a025..08e5a9d 100644
--- a/PowerKit/Utils/Extensions/TimeSpanExtensions.cs
+++ b/PowerKit/Extensions/TimeSpanExtensions.cs
@@ -1,6 +1,6 @@
using System;
-namespace PowerKit.Utils.Extensions;
+namespace PowerKit.Extensions;
internal static class TimeSpanExtensions
{
From e5886168349de7951032cf792ddf2b2b96e51ff7 Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Tue, 7 Apr 2026 19:41:43 +0300
Subject: [PATCH 04/43] Change copyright year to 2026
Updated copyright year in the License file.
---
License.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/License.txt b/License.txt
index 321d06a..68c3d52 100644
--- a/License.txt
+++ b/License.txt
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2023-2026 Oleksii Holub
+Copyright (c) 2026 Oleksii Holub
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 8c4b9aa99f0ab9f887e60ad1f91cfd6cf9d22e53 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 16:49:09 +0000
Subject: [PATCH 05/43] =?UTF-8?q?refactor:=20address=20review=20feedback?=
=?UTF-8?q?=20=E2=80=94=20rename=20classes,=20split=20files,=20netstandard?=
=?UTF-8?q?2.0,=20remove=20props?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/7af97092-2f8d-463c-8357-7b2ac9afa654
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
Directory.Build.props | 2 +-
PowerKit.slnx | 8 ++++
.../AggregateExceptionExtensions.cs | 16 +++++++
...nsions.cs => AsyncEnumerableExtensions.cs} | 2 +-
...nExtensions.cs => EnumerableExtensions.cs} | 14 ++----
PowerKit/Extensions/ExceptionExtensions.cs | 44 +++++++------------
...cExtensions.cs => FunctionalExtensions.cs} | 2 +-
PowerKit/Extensions/ObjectExtensions.cs | 14 ++++++
PowerKit/Extensions/PathExtensions.cs | 10 ++---
.../Extensions/StringBuilderExtensions.cs | 12 +++++
PowerKit/Extensions/StringExtensions.cs | 6 ---
PowerKit/PowerKit.csproj | 10 +++--
PowerKit/PowerKit.props | 5 ---
13 files changed, 82 insertions(+), 63 deletions(-)
create mode 100644 PowerKit/Extensions/AggregateExceptionExtensions.cs
rename PowerKit/Extensions/{AsyncCollectionExtensions.cs => AsyncEnumerableExtensions.cs} (96%)
rename PowerKit/Extensions/{CollectionExtensions.cs => EnumerableExtensions.cs} (82%)
rename PowerKit/Extensions/{GenericExtensions.cs => FunctionalExtensions.cs} (91%)
create mode 100644 PowerKit/Extensions/ObjectExtensions.cs
create mode 100644 PowerKit/Extensions/StringBuilderExtensions.cs
delete mode 100644 PowerKit/PowerKit.props
diff --git a/Directory.Build.props b/Directory.Build.props
index 50a8b42..85c9b94 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,7 +11,7 @@
$(Company)
- Collection of shared utility extensions for Tyrrrz projects.
+ Collection of utilities and extensions for rapid .NET development
utils extensions utilities source
https://github.com/Tyrrrz/PowerKit
https://github.com/Tyrrrz/PowerKit/releases
diff --git a/PowerKit.slnx b/PowerKit.slnx
index 59e4068..fbcbc92 100644
--- a/PowerKit.slnx
+++ b/PowerKit.slnx
@@ -1,3 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/PowerKit/Extensions/AggregateExceptionExtensions.cs b/PowerKit/Extensions/AggregateExceptionExtensions.cs
new file mode 100644
index 0000000..b0aaaaf
--- /dev/null
+++ b/PowerKit/Extensions/AggregateExceptionExtensions.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Linq;
+
+namespace PowerKit.Extensions;
+
+internal static class AggregateExceptionExtensions
+{
+ extension(AggregateException exception)
+ {
+ public Exception? TryGetSingle()
+ {
+ var exceptions = exception.Flatten().InnerExceptions;
+ return exceptions.Count == 1 ? exceptions.Single() : null;
+ }
+ }
+}
diff --git a/PowerKit/Extensions/AsyncCollectionExtensions.cs b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
similarity index 96%
rename from PowerKit/Extensions/AsyncCollectionExtensions.cs
rename to PowerKit/Extensions/AsyncEnumerableExtensions.cs
index 86ed232..e5b56d0 100644
--- a/PowerKit/Extensions/AsyncCollectionExtensions.cs
+++ b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
@@ -5,7 +5,7 @@
namespace PowerKit.Extensions;
-internal static class AsyncCollectionExtensions
+internal static class AsyncEnumerableExtensions
{
extension(IAsyncEnumerable source)
{
diff --git a/PowerKit/Extensions/CollectionExtensions.cs b/PowerKit/Extensions/EnumerableExtensions.cs
similarity index 82%
rename from PowerKit/Extensions/CollectionExtensions.cs
rename to PowerKit/Extensions/EnumerableExtensions.cs
index 544bd32..fa147c2 100644
--- a/PowerKit/Extensions/CollectionExtensions.cs
+++ b/PowerKit/Extensions/EnumerableExtensions.cs
@@ -3,16 +3,8 @@
namespace PowerKit.Extensions;
-internal static class CollectionExtensions
+internal static class EnumerableExtensions
{
- extension(T obj)
- {
- public IEnumerable ToSingletonEnumerable()
- {
- yield return obj;
- }
- }
-
extension(IEnumerable source)
where T : class
{
@@ -46,7 +38,7 @@ public IEnumerable WhereNotNullOrWhiteSpace()
foreach (var item in source)
{
if (!string.IsNullOrWhiteSpace(item))
- yield return item;
+ yield return item!;
}
}
}
@@ -65,7 +57,7 @@ public IEnumerable WhereNotNullOrWhiteSpace()
public T? ElementAtOrNull(int index)
{
var list = source as IReadOnlyList ?? source.ToArray();
- return index < list.Count ? list[index] : null;
+ return index >= 0 && index < list.Count ? list[index] : null;
}
}
}
diff --git a/PowerKit/Extensions/ExceptionExtensions.cs b/PowerKit/Extensions/ExceptionExtensions.cs
index 0b13f52..2c98a95 100644
--- a/PowerKit/Extensions/ExceptionExtensions.cs
+++ b/PowerKit/Extensions/ExceptionExtensions.cs
@@ -1,44 +1,34 @@
using System;
using System.Collections.Generic;
-using System.Linq;
namespace PowerKit.Extensions;
internal static class ExceptionExtensions
{
- extension(AggregateException exception)
- {
- public Exception? TryGetSingle()
- {
- var exceptions = exception.Flatten().InnerExceptions;
- return exceptions.Count == 1 ? exceptions.Single() : null;
- }
- }
-
extension(Exception exception)
{
- private void PopulateDescendants(ICollection result)
+ public IReadOnlyList GetSelfAndDescendants()
{
- if (exception is AggregateException aggregateException)
+ var result = new List { exception };
+ PopulateDescendants(exception, result);
+ return result;
+
+ static void PopulateDescendants(Exception ex, ICollection result)
{
- foreach (var innerException in aggregateException.InnerExceptions)
+ if (ex is AggregateException aggregateException)
{
- result.Add(innerException);
- innerException.PopulateDescendants(result);
+ foreach (var innerException in aggregateException.InnerExceptions)
+ {
+ result.Add(innerException);
+ PopulateDescendants(innerException, result);
+ }
+ }
+ else if (ex.InnerException is not null)
+ {
+ result.Add(ex.InnerException);
+ PopulateDescendants(ex.InnerException, result);
}
}
- else if (exception.InnerException is not null)
- {
- result.Add(exception.InnerException);
- exception.InnerException.PopulateDescendants(result);
- }
- }
-
- public IReadOnlyList GetSelfAndDescendants()
- {
- var result = new List { exception };
- exception.PopulateDescendants(result);
- return result;
}
}
}
diff --git a/PowerKit/Extensions/GenericExtensions.cs b/PowerKit/Extensions/FunctionalExtensions.cs
similarity index 91%
rename from PowerKit/Extensions/GenericExtensions.cs
rename to PowerKit/Extensions/FunctionalExtensions.cs
index e69e82c..8b8d49d 100644
--- a/PowerKit/Extensions/GenericExtensions.cs
+++ b/PowerKit/Extensions/FunctionalExtensions.cs
@@ -3,7 +3,7 @@
namespace PowerKit.Extensions;
-internal static class GenericExtensions
+internal static class FunctionalExtensions
{
extension(TIn input)
{
diff --git a/PowerKit/Extensions/ObjectExtensions.cs b/PowerKit/Extensions/ObjectExtensions.cs
new file mode 100644
index 0000000..4225646
--- /dev/null
+++ b/PowerKit/Extensions/ObjectExtensions.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace PowerKit.Extensions;
+
+internal static class ObjectExtensions
+{
+ extension(T obj)
+ {
+ public IEnumerable ToSingletonEnumerable()
+ {
+ yield return obj;
+ }
+ }
+}
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index 2a83ee4..b95e3f7 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
@@ -34,12 +33,9 @@ public static string EscapeFileName(string path)
foreach (var c in path)
buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
- // File names cannot end with a dot on Windows
- if (OperatingSystem.IsWindows())
- {
- while (buffer.Length > 0 && buffer[^1] == '.')
- buffer.Remove(buffer.Length - 1, 1);
- }
+ // File names cannot end with a dot (invalid on Windows, ambiguous on other filesystems)
+ while (buffer.Length > 0 && buffer[buffer.Length - 1] == '.')
+ buffer.Remove(buffer.Length - 1, 1);
return buffer.ToString();
}
diff --git a/PowerKit/Extensions/StringBuilderExtensions.cs b/PowerKit/Extensions/StringBuilderExtensions.cs
new file mode 100644
index 0000000..15ef10f
--- /dev/null
+++ b/PowerKit/Extensions/StringBuilderExtensions.cs
@@ -0,0 +1,12 @@
+using System.Text;
+
+namespace PowerKit.Extensions;
+
+internal static class StringBuilderExtensions
+{
+ extension(StringBuilder builder)
+ {
+ public StringBuilder AppendIfNotEmpty(char value) =>
+ builder.Length > 0 ? builder.Append(value) : builder;
+ }
+}
diff --git a/PowerKit/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs
index 9e7790b..57ef073 100644
--- a/PowerKit/Extensions/StringExtensions.cs
+++ b/PowerKit/Extensions/StringExtensions.cs
@@ -45,10 +45,4 @@ public string ToSpaceSeparatedWords()
return builder.ToString();
}
}
-
- extension(StringBuilder builder)
- {
- public StringBuilder AppendIfNotEmpty(char value) =>
- builder.Length > 0 ? builder.Append(value) : builder;
- }
}
diff --git a/PowerKit/PowerKit.csproj b/PowerKit/PowerKit.csproj
index 7a27e2e..14d8aaf 100644
--- a/PowerKit/PowerKit.csproj
+++ b/PowerKit/PowerKit.csproj
@@ -1,8 +1,6 @@
-
-
- net9.0
+ netstandard2.0
true
@@ -13,7 +11,11 @@
-
+
+
+
+
+
-
- preview
-
-
From 1c7387300dee538e0112c4d00613d2d7cdc3d780 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 17:01:30 +0000
Subject: [PATCH 06/43] =?UTF-8?q?fix:=20address=20automated=20review=20fee?=
=?UTF-8?q?dback=20=E2=80=94=20TakeAsync=20guard,=20ConfigureAwait,=20CanS?=
=?UTF-8?q?eek,=20fileName=20param?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/f217b614-b393-402a-857b-37113c1e00d1
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
.../Extensions/AsyncEnumerableExtensions.cs | 21 ++++++++++++++++---
PowerKit/Extensions/PathExtensions.cs | 6 +++---
PowerKit/Extensions/StreamExtensions.cs | 4 +++-
3 files changed, 24 insertions(+), 7 deletions(-)
diff --git a/PowerKit/Extensions/AsyncEnumerableExtensions.cs b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
index e5b56d0..a6764bb 100644
--- a/PowerKit/Extensions/AsyncEnumerableExtensions.cs
+++ b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
@@ -14,9 +14,16 @@ public async IAsyncEnumerable TakeAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
+ if (count <= 0)
+ yield break;
+
var currentCount = 0;
- await foreach (var item in source.WithCancellation(cancellationToken))
+ await foreach (
+ var item in source
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false)
+ )
{
if (currentCount >= count)
yield break;
@@ -31,7 +38,11 @@ public async IAsyncEnumerable SelectManyAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
- await foreach (var item in source.WithCancellation(cancellationToken))
+ await foreach (
+ var item in source
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false)
+ )
foreach (var result in transform(item))
yield return result;
}
@@ -42,7 +53,11 @@ public async ValueTask> ToListAsync(
{
var list = new List();
- await foreach (var item in source.WithCancellation(cancellationToken))
+ await foreach (
+ var item in source
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false)
+ )
list.Add(item);
return list;
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index b95e3f7..c962be3 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -26,11 +26,11 @@ internal static class PathExtensions
extension(Path)
{
- public static string EscapeFileName(string path)
+ public static string EscapeFileName(string fileName)
{
- var buffer = new StringBuilder(path.Length);
+ var buffer = new StringBuilder(fileName.Length);
- foreach (var c in path)
+ foreach (var c in fileName)
buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
// File names cannot end with a dot (invalid on Windows, ambiguous on other filesystems)
diff --git a/PowerKit/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs
index 2e5a37b..6925eed 100644
--- a/PowerKit/Extensions/StreamExtensions.cs
+++ b/PowerKit/Extensions/StreamExtensions.cs
@@ -60,7 +60,9 @@ await destination
.ConfigureAwait(false);
totalBytesRead += bytesRead;
- progress?.Report(1.0 * totalBytesRead / source.Length);
+
+ if (progress is not null && source.CanSeek && source.Length > 0)
+ progress.Report(1.0 * totalBytesRead / source.Length);
}
}
}
From 24c1daee1baf37e7aa68d45244edf2c76bbc7f7e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 17:12:32 +0000
Subject: [PATCH 07/43] test: add PowerKit.Tests xunit project with 90 tests
covering all utilities
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/353694f3-2e07-4be8-825a-8e291bb23a2b
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
.../AggregateExceptionExtensionsTests.cs | 42 +++++++
.../AsyncEnumerableExtensionsTests.cs | 99 ++++++++++++++++
PowerKit.Tests/DisposableTests.cs | 53 +++++++++
PowerKit.Tests/EnumerableExtensionsTests.cs | 99 ++++++++++++++++
PowerKit.Tests/ExceptionExtensionsTests.cs | 61 ++++++++++
PowerKit.Tests/FunctionalExtensionsTests.cs | 61 ++++++++++
PowerKit.Tests/ObjectExtensionsTests.cs | 29 +++++
PowerKit.Tests/PathExtensionsTests.cs | 61 ++++++++++
PowerKit.Tests/PowerKit.Tests.csproj | 27 +++++
PowerKit.Tests/StreamExtensionsTests.cs | 75 ++++++++++++
.../StringBuilderExtensionsTests.cs | 31 +++++
PowerKit.Tests/StringExtensionsTests.cs | 108 ++++++++++++++++++
PowerKit.Tests/TextReaderExtensionsTests.cs | 31 +++++
PowerKit.Tests/TimeSpanExtensionsTests.cs | 46 ++++++++
PowerKit.slnx | 1 +
15 files changed, 824 insertions(+)
create mode 100644 PowerKit.Tests/AggregateExceptionExtensionsTests.cs
create mode 100644 PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
create mode 100644 PowerKit.Tests/DisposableTests.cs
create mode 100644 PowerKit.Tests/EnumerableExtensionsTests.cs
create mode 100644 PowerKit.Tests/ExceptionExtensionsTests.cs
create mode 100644 PowerKit.Tests/FunctionalExtensionsTests.cs
create mode 100644 PowerKit.Tests/ObjectExtensionsTests.cs
create mode 100644 PowerKit.Tests/PathExtensionsTests.cs
create mode 100644 PowerKit.Tests/PowerKit.Tests.csproj
create mode 100644 PowerKit.Tests/StreamExtensionsTests.cs
create mode 100644 PowerKit.Tests/StringBuilderExtensionsTests.cs
create mode 100644 PowerKit.Tests/StringExtensionsTests.cs
create mode 100644 PowerKit.Tests/TextReaderExtensionsTests.cs
create mode 100644 PowerKit.Tests/TimeSpanExtensionsTests.cs
diff --git a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
new file mode 100644
index 0000000..c63ff8d
--- /dev/null
+++ b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
@@ -0,0 +1,42 @@
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class AggregateExceptionExtensionsTests
+{
+ [Fact]
+ public void TryGetSingle_SingleInnerException_ReturnsIt()
+ {
+ var inner = new Exception("only");
+ var aggregate = new AggregateException(inner);
+
+ Assert.Same(inner, aggregate.TryGetSingle());
+ }
+
+ [Fact]
+ public void TryGetSingle_MultipleInnerExceptions_ReturnsNull()
+ {
+ var aggregate = new AggregateException(new Exception("a"), new Exception("b"));
+
+ Assert.Null(aggregate.TryGetSingle());
+ }
+
+ [Fact]
+ public void TryGetSingle_NestedAggregateWithOneLeaf_ReturnsLeaf()
+ {
+ var leaf = new Exception("leaf");
+ var nested = new AggregateException(leaf);
+ var outer = new AggregateException(nested);
+
+ Assert.Same(leaf, outer.TryGetSingle());
+ }
+
+ [Fact]
+ public void TryGetSingle_NestedAggregateWithMultipleLeaves_ReturnsNull()
+ {
+ var nested = new AggregateException(new Exception("a"), new Exception("b"));
+ var outer = new AggregateException(nested);
+
+ Assert.Null(outer.TryGetSingle());
+ }
+}
diff --git a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
new file mode 100644
index 0000000..f2e8462
--- /dev/null
+++ b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
@@ -0,0 +1,99 @@
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class AsyncEnumerableExtensionsTests
+{
+ private static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable source)
+ {
+ foreach (var item in source)
+ yield return item;
+ }
+
+ [Fact]
+ public async Task TakeAsync_ZeroCount_ReturnsEmpty()
+ {
+ var source = ToAsyncEnumerable([1, 2, 3]);
+ var result = await source.TakeAsync(0).ToListAsync();
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public async Task TakeAsync_NegativeCount_ReturnsEmpty()
+ {
+ var source = ToAsyncEnumerable([1, 2, 3]);
+ var result = await source.TakeAsync(-1).ToListAsync();
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public async Task TakeAsync_CountLessThanSource_ReturnsThatMany()
+ {
+ var source = ToAsyncEnumerable([1, 2, 3, 4, 5]);
+ var result = await source.TakeAsync(3).ToListAsync();
+ Assert.Equal([1, 2, 3], result);
+ }
+
+ [Fact]
+ public async Task TakeAsync_CountGreaterThanSource_ReturnsAll()
+ {
+ var source = ToAsyncEnumerable([1, 2, 3]);
+ var result = await source.TakeAsync(10).ToListAsync();
+ Assert.Equal([1, 2, 3], result);
+ }
+
+ [Fact]
+ public async Task TakeAsync_ZeroCount_DoesNotConsumeAnyElements()
+ {
+ var consumed = 0;
+
+ async IAsyncEnumerable Tracked()
+ {
+ consumed++;
+ yield return 1;
+ }
+
+ await Tracked().TakeAsync(0).ToListAsync();
+ Assert.Equal(0, consumed);
+ }
+
+ [Fact]
+ public async Task SelectManyAsync_FlattensResults()
+ {
+ var source = ToAsyncEnumerable(["ab", "cd", "ef"]);
+ var result = await source.SelectManyAsync(s => s.ToCharArray()).ToListAsync();
+ Assert.Equal(['a', 'b', 'c', 'd', 'e', 'f'], result);
+ }
+
+ [Fact]
+ public async Task SelectManyAsync_EmptySource_ReturnsEmpty()
+ {
+ var source = ToAsyncEnumerable(Array.Empty());
+ var result = await source.SelectManyAsync(s => s.ToCharArray()).ToListAsync();
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public async Task ToListAsync_CollectsAllElements()
+ {
+ var source = ToAsyncEnumerable([1, 2, 3]);
+ var result = await source.ToListAsync();
+ Assert.Equal([1, 2, 3], result);
+ }
+
+ [Fact]
+ public async Task ToListAsync_EmptySource_ReturnsEmptyList()
+ {
+ var source = ToAsyncEnumerable(Array.Empty());
+ var result = await source.ToListAsync();
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public async Task GetAwaiter_DirectAwait_ReturnsAllElements()
+ {
+ var source = ToAsyncEnumerable([10, 20, 30]);
+ var result = await source;
+ Assert.Equal([10, 20, 30], result);
+ }
+}
diff --git a/PowerKit.Tests/DisposableTests.cs b/PowerKit.Tests/DisposableTests.cs
new file mode 100644
index 0000000..5643353
--- /dev/null
+++ b/PowerKit.Tests/DisposableTests.cs
@@ -0,0 +1,53 @@
+using PowerKit;
+
+namespace PowerKit.Tests;
+
+public class DisposableTests
+{
+ [Fact]
+ public void Null_CanBeDisposedWithoutEffect()
+ {
+ // Should not throw
+ Disposable.Null.Dispose();
+ }
+
+ [Fact]
+ public void Create_InvokesActionOnDispose()
+ {
+ var invoked = false;
+ var disposable = Disposable.Create(() => invoked = true);
+
+ Assert.False(invoked);
+ disposable.Dispose();
+ Assert.True(invoked);
+ }
+
+ [Fact]
+ public void Merge_DisposesAllItems()
+ {
+ var count = 0;
+ var disposables = Enumerable
+ .Range(0, 3)
+ .Select(_ => Disposable.Create(() => count++))
+ .ToArray();
+
+ var merged = Disposable.Merge(disposables);
+ merged.Dispose();
+
+ Assert.Equal(3, count);
+ }
+
+ [Fact]
+ public void Merge_DisposesInOrder()
+ {
+ var order = new List();
+ var disposables = Enumerable
+ .Range(0, 3)
+ .Select(i => Disposable.Create(() => order.Add(i)))
+ .ToArray();
+
+ Disposable.Merge(disposables).Dispose();
+
+ Assert.Equal([0, 1, 2], order);
+ }
+}
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
new file mode 100644
index 0000000..b2aa0e3
--- /dev/null
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -0,0 +1,99 @@
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class EnumerableExtensionsTests
+{
+ [Fact]
+ public void WhereNotNull_ReferenceType_FiltersNulls()
+ {
+ string?[] source = ["a", null, "b", null, "c"];
+ Assert.Equal(["a", "b", "c"], source.WhereNotNull());
+ }
+
+ [Fact]
+ public void WhereNotNull_ReferenceType_EmptySource_ReturnsEmpty()
+ {
+ Assert.Empty(Array.Empty().WhereNotNull());
+ }
+
+ [Fact]
+ public void WhereNotNull_NullableStruct_FiltersNulls()
+ {
+ int?[] source = [1, null, 2, null, 3];
+ Assert.Equal([1, 2, 3], source.WhereNotNull());
+ }
+
+ [Fact]
+ public void WhereNotNull_NullableStruct_EmptySource_ReturnsEmpty()
+ {
+ Assert.Empty(Array.Empty().WhereNotNull());
+ }
+
+ [Fact]
+ public void WhereNotNullOrWhiteSpace_FiltersNullsAndWhitespace()
+ {
+ string?[] source = ["hello", null, " ", "", "world"];
+ Assert.Equal(["hello", "world"], source.WhereNotNullOrWhiteSpace());
+ }
+
+ [Fact]
+ public void WhereNotNullOrWhiteSpace_EmptySource_ReturnsEmpty()
+ {
+ Assert.Empty(Array.Empty().WhereNotNullOrWhiteSpace());
+ }
+
+ [Fact]
+ public void FirstOrNull_NonEmptySource_ReturnsFirst()
+ {
+ int[] source = [5, 10, 15];
+ Assert.Equal(5, source.FirstOrNull());
+ }
+
+ [Fact]
+ public void FirstOrNull_EmptySource_ReturnsNull()
+ {
+ Assert.Null(Array.Empty().FirstOrNull());
+ }
+
+ [Fact]
+ public void ElementAtOrNull_ValidIndex_ReturnsElement()
+ {
+ int[] source = [10, 20, 30];
+ Assert.Equal(20, source.ElementAtOrNull(1));
+ }
+
+ [Fact]
+ public void ElementAtOrNull_IndexZero_ReturnsFirst()
+ {
+ int[] source = [42, 99];
+ Assert.Equal(42, source.ElementAtOrNull(0));
+ }
+
+ [Fact]
+ public void ElementAtOrNull_IndexAtLastElement_ReturnsLast()
+ {
+ int[] source = [1, 2, 3];
+ Assert.Equal(3, source.ElementAtOrNull(2));
+ }
+
+ [Fact]
+ public void ElementAtOrNull_IndexOutOfRange_ReturnsNull()
+ {
+ int[] source = [1, 2, 3];
+ Assert.Null(source.ElementAtOrNull(10));
+ }
+
+ [Fact]
+ public void ElementAtOrNull_NegativeIndex_ReturnsNull()
+ {
+ int[] source = [1, 2, 3];
+ Assert.Null(source.ElementAtOrNull(-1));
+ }
+
+ [Fact]
+ public void ElementAtOrNull_EmptySource_ReturnsNull()
+ {
+ Assert.Null(Array.Empty().ElementAtOrNull(0));
+ }
+}
diff --git a/PowerKit.Tests/ExceptionExtensionsTests.cs b/PowerKit.Tests/ExceptionExtensionsTests.cs
new file mode 100644
index 0000000..b3be599
--- /dev/null
+++ b/PowerKit.Tests/ExceptionExtensionsTests.cs
@@ -0,0 +1,61 @@
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class ExceptionExtensionsTests
+{
+ [Fact]
+ public void GetSelfAndDescendants_NoInner_ReturnsSelf()
+ {
+ var ex = new Exception("root");
+ var result = ex.GetSelfAndDescendants();
+ Assert.Single(result, ex);
+ }
+
+ [Fact]
+ public void GetSelfAndDescendants_WithInnerException_ReturnsSelfAndInner()
+ {
+ var inner = new Exception("inner");
+ var outer = new Exception("outer", inner);
+
+ var result = outer.GetSelfAndDescendants();
+
+ Assert.Equal([outer, inner], result);
+ }
+
+ [Fact]
+ public void GetSelfAndDescendants_WithChainedInnerExceptions_ReturnsAll()
+ {
+ var leaf = new Exception("leaf");
+ var middle = new Exception("middle", leaf);
+ var root = new Exception("root", middle);
+
+ var result = root.GetSelfAndDescendants();
+
+ Assert.Equal([root, middle, leaf], result);
+ }
+
+ [Fact]
+ public void GetSelfAndDescendants_WithAggregateException_FlattensInnerExceptions()
+ {
+ var inner1 = new Exception("inner1");
+ var inner2 = new Exception("inner2");
+ var aggregate = new AggregateException("aggregate", inner1, inner2);
+
+ var result = aggregate.GetSelfAndDescendants();
+
+ Assert.Equal([aggregate, inner1, inner2], result);
+ }
+
+ [Fact]
+ public void GetSelfAndDescendants_NestedAggregateException_ReturnsAllDescendants()
+ {
+ var leaf = new Exception("leaf");
+ var inner = new AggregateException("inner", leaf);
+ var root = new AggregateException("root", inner);
+
+ var result = root.GetSelfAndDescendants();
+
+ Assert.Equal([root, inner, leaf], result);
+ }
+}
diff --git a/PowerKit.Tests/FunctionalExtensionsTests.cs b/PowerKit.Tests/FunctionalExtensionsTests.cs
new file mode 100644
index 0000000..527c995
--- /dev/null
+++ b/PowerKit.Tests/FunctionalExtensionsTests.cs
@@ -0,0 +1,61 @@
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class FunctionalExtensionsTests
+{
+ [Fact]
+ public void Pipe_TransformsValue()
+ {
+ var result = 5.Pipe(x => x * 2);
+ Assert.Equal(10, result);
+ }
+
+ [Fact]
+ public void Pipe_ChainedCalls_AppliesInOrder()
+ {
+ var result = "hello".Pipe(s => s.ToUpper()).Pipe(s => s + "!");
+ Assert.Equal("HELLO!", result);
+ }
+
+ [Fact]
+ public void NullIf_PredicateMatches_ReturnsNull()
+ {
+ int value = 0;
+ Assert.Null(value.NullIf(v => v == 0));
+ }
+
+ [Fact]
+ public void NullIf_PredicateDoesNotMatch_ReturnsValue()
+ {
+ int value = 5;
+ Assert.Equal(5, value.NullIf(v => v == 0));
+ }
+
+ [Fact]
+ public void NullIfDefault_DefaultValue_ReturnsNull()
+ {
+ int value = 0;
+ Assert.Null(value.NullIfDefault());
+ }
+
+ [Fact]
+ public void NullIfDefault_NonDefaultValue_ReturnsValue()
+ {
+ int value = 42;
+ Assert.Equal(42, value.NullIfDefault());
+ }
+
+ [Fact]
+ public void NullIfDefault_DefaultGuid_ReturnsNull()
+ {
+ Assert.Null(Guid.Empty.NullIfDefault());
+ }
+
+ [Fact]
+ public void NullIfDefault_NonDefaultGuid_ReturnsValue()
+ {
+ var id = Guid.NewGuid();
+ Assert.Equal(id, id.NullIfDefault());
+ }
+}
diff --git a/PowerKit.Tests/ObjectExtensionsTests.cs b/PowerKit.Tests/ObjectExtensionsTests.cs
new file mode 100644
index 0000000..7ac4128
--- /dev/null
+++ b/PowerKit.Tests/ObjectExtensionsTests.cs
@@ -0,0 +1,29 @@
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class ObjectExtensionsTests
+{
+ [Fact]
+ public void ToSingletonEnumerable_ReturnsEnumerableWithSingleElement()
+ {
+ var result = 42.ToSingletonEnumerable().ToList();
+ Assert.Single(result, 42);
+ }
+
+ [Fact]
+ public void ToSingletonEnumerable_WorksWithReferenceType()
+ {
+ var obj = "hello";
+ var result = obj.ToSingletonEnumerable().ToList();
+ Assert.Single(result, "hello");
+ }
+
+ [Fact]
+ public void ToSingletonEnumerable_WorksWithNull()
+ {
+ string? obj = null;
+ var result = obj.ToSingletonEnumerable().ToList();
+ Assert.Single(result, (string?)null);
+ }
+}
diff --git a/PowerKit.Tests/PathExtensionsTests.cs b/PowerKit.Tests/PathExtensionsTests.cs
new file mode 100644
index 0000000..7295c54
--- /dev/null
+++ b/PowerKit.Tests/PathExtensionsTests.cs
@@ -0,0 +1,61 @@
+using System.IO;
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class PathExtensionsTests
+{
+ [Fact]
+ public void EscapeFileName_ValidName_ReturnsUnchanged()
+ {
+ Assert.Equal("hello world.txt", Path.EscapeFileName("hello world.txt"));
+ }
+
+ [Fact]
+ public void EscapeFileName_ReplacesForwardSlash()
+ {
+ Assert.Equal("a_b", Path.EscapeFileName("a/b"));
+ }
+
+ [Fact]
+ public void EscapeFileName_ReplacesBackslash()
+ {
+ Assert.Equal("a_b", Path.EscapeFileName("a\\b"));
+ }
+
+ [Fact]
+ public void EscapeFileName_ReplacesColon()
+ {
+ Assert.Equal("C_drive", Path.EscapeFileName("C:drive"));
+ }
+
+ [Fact]
+ public void EscapeFileName_ReplacesAllInvalidChars()
+ {
+ Assert.Equal("a_b_c_d_e_f_g_h_i", Path.EscapeFileName("a\0b/c\\d:e*f?g\"h
+
+
+ net9.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
new file mode 100644
index 0000000..d93a95f
--- /dev/null
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -0,0 +1,75 @@
+using System.IO;
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class StreamExtensionsTests
+{
+ [Fact]
+ public async Task CopyToAsync_WithAutoFlush_CopiesAllBytes()
+ {
+ var data = new byte[] { 1, 2, 3, 4, 5 };
+ using var source = new MemoryStream(data);
+ using var destination = new MemoryStream();
+
+ await source.CopyToAsync(destination, autoFlush: true);
+
+ Assert.Equal(data, destination.ToArray());
+ }
+
+ [Fact]
+ public async Task CopyToAsync_WithoutAutoFlush_CopiesAllBytes()
+ {
+ var data = new byte[] { 10, 20, 30 };
+ using var source = new MemoryStream(data);
+ using var destination = new MemoryStream();
+
+ await source.CopyToAsync(destination, autoFlush: false);
+
+ Assert.Equal(data, destination.ToArray());
+ }
+
+ [Fact]
+ public async Task CopyToAsync_EmptySource_ProducesEmptyDestination()
+ {
+ using var source = new MemoryStream();
+ using var destination = new MemoryStream();
+
+ await source.CopyToAsync(destination, autoFlush: false);
+
+ Assert.Empty(destination.ToArray());
+ }
+
+ [Fact]
+ public async Task CopyToAsync_WithProgress_CopiesAllBytes()
+ {
+ var data = new byte[1024];
+ new Random(0).NextBytes(data);
+ using var source = new MemoryStream(data);
+ using var destination = new MemoryStream();
+
+ await source.CopyToAsync(destination, progress: null);
+
+ Assert.Equal(data, destination.ToArray());
+ }
+
+ [Fact]
+ public async Task CopyToAsync_WithProgress_ReportsProgress()
+ {
+ var data = new byte[1024];
+ using var source = new MemoryStream(data);
+ using var destination = new MemoryStream();
+
+ var reports = new List();
+ var progress = new Progress(v => reports.Add(v));
+
+ await source.CopyToAsync(destination, progress: progress);
+
+ // Allow Progress callbacks to fire on the thread pool
+ await Task.Delay(50);
+
+ Assert.NotEmpty(reports);
+ Assert.All(reports, v => Assert.InRange(v, 0.0, 1.0));
+ Assert.Equal(1.0, reports[^1], precision: 5);
+ }
+}
diff --git a/PowerKit.Tests/StringBuilderExtensionsTests.cs b/PowerKit.Tests/StringBuilderExtensionsTests.cs
new file mode 100644
index 0000000..abf3295
--- /dev/null
+++ b/PowerKit.Tests/StringBuilderExtensionsTests.cs
@@ -0,0 +1,31 @@
+using System.Text;
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class StringBuilderExtensionsTests
+{
+ [Fact]
+ public void AppendIfNotEmpty_EmptyBuilder_DoesNotAppend()
+ {
+ var builder = new StringBuilder();
+ builder.AppendIfNotEmpty(',');
+ Assert.Equal("", builder.ToString());
+ }
+
+ [Fact]
+ public void AppendIfNotEmpty_NonEmptyBuilder_Appends()
+ {
+ var builder = new StringBuilder("hello");
+ builder.AppendIfNotEmpty(',');
+ Assert.Equal("hello,", builder.ToString());
+ }
+
+ [Fact]
+ public void AppendIfNotEmpty_ReturnsBuilder_AllowsChaining()
+ {
+ var builder = new StringBuilder("a");
+ builder.AppendIfNotEmpty(',').Append("b");
+ Assert.Equal("a,b", builder.ToString());
+ }
+}
diff --git a/PowerKit.Tests/StringExtensionsTests.cs b/PowerKit.Tests/StringExtensionsTests.cs
new file mode 100644
index 0000000..d3d4080
--- /dev/null
+++ b/PowerKit.Tests/StringExtensionsTests.cs
@@ -0,0 +1,108 @@
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class StringExtensionsTests
+{
+ [Fact]
+ public void NullIfWhiteSpace_NonWhitespaceString_ReturnsSame()
+ {
+ Assert.Equal("hello", "hello".NullIfWhiteSpace());
+ }
+
+ [Fact]
+ public void NullIfWhiteSpace_WhitespaceOnly_ReturnsNull()
+ {
+ Assert.Null(" ".NullIfWhiteSpace());
+ }
+
+ [Fact]
+ public void NullIfWhiteSpace_EmptyString_ReturnsNull()
+ {
+ Assert.Null("".NullIfWhiteSpace());
+ }
+
+ [Fact]
+ public void SubstringUntil_SubstringFound_ReturnsBeforeIt()
+ {
+ Assert.Equal("hello", "hello world".SubstringUntil(" "));
+ }
+
+ [Fact]
+ public void SubstringUntil_SubstringNotFound_ReturnsFullString()
+ {
+ Assert.Equal("hello", "hello".SubstringUntil("x"));
+ }
+
+ [Fact]
+ public void SubstringUntil_SubstringAtStart_ReturnsEmpty()
+ {
+ Assert.Equal("", "xhello".SubstringUntil("x"));
+ }
+
+ [Fact]
+ public void SubstringAfter_SubstringFound_ReturnsAfterIt()
+ {
+ Assert.Equal("world", "hello world".SubstringAfter(" "));
+ }
+
+ [Fact]
+ public void SubstringAfter_SubstringNotFound_ReturnsEmpty()
+ {
+ Assert.Equal("", "hello".SubstringAfter("x"));
+ }
+
+ [Fact]
+ public void SubstringAfter_SubstringAtEnd_ReturnsEmpty()
+ {
+ Assert.Equal("", "hellox".SubstringAfter("x"));
+ }
+
+ [Fact]
+ public void Truncate_StringShorterThanLimit_ReturnsFull()
+ {
+ Assert.Equal("hi", "hi".Truncate(10));
+ }
+
+ [Fact]
+ public void Truncate_StringExactlyAtLimit_ReturnsFull()
+ {
+ Assert.Equal("hello", "hello".Truncate(5));
+ }
+
+ [Fact]
+ public void Truncate_StringLongerThanLimit_ReturnsTruncated()
+ {
+ Assert.Equal("hel", "hello".Truncate(3));
+ }
+
+ [Fact]
+ public void ToSpaceSeparatedWords_PascalCase_InsertsSpacesBeforeUppercase()
+ {
+ Assert.Equal("Hello World", "HelloWorld".ToSpaceSeparatedWords());
+ }
+
+ [Fact]
+ public void ToSpaceSeparatedWords_SingleWord_ReturnsUnchanged()
+ {
+ Assert.Equal("Hello", "Hello".ToSpaceSeparatedWords());
+ }
+
+ [Fact]
+ public void ToSpaceSeparatedWords_AllLowercase_ReturnsUnchanged()
+ {
+ Assert.Equal("hello", "hello".ToSpaceSeparatedWords());
+ }
+
+ [Fact]
+ public void ToSpaceSeparatedWords_EmptyString_ReturnsEmpty()
+ {
+ Assert.Equal("", "".ToSpaceSeparatedWords());
+ }
+
+ [Fact]
+ public void ToSpaceSeparatedWords_MultipleWords_SplitsCorrectly()
+ {
+ Assert.Equal("Foo Bar Baz", "FooBarBaz".ToSpaceSeparatedWords());
+ }
+}
diff --git a/PowerKit.Tests/TextReaderExtensionsTests.cs b/PowerKit.Tests/TextReaderExtensionsTests.cs
new file mode 100644
index 0000000..61d7e6b
--- /dev/null
+++ b/PowerKit.Tests/TextReaderExtensionsTests.cs
@@ -0,0 +1,31 @@
+using System.IO;
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class TextReaderExtensionsTests
+{
+ [Fact]
+ public async Task ReadLinesAsync_ReadsAllLines()
+ {
+ using var reader = new StringReader("line1\nline2\nline3");
+ var lines = await reader.ReadLinesAsync().ToListAsync();
+ Assert.Equal(["line1", "line2", "line3"], lines);
+ }
+
+ [Fact]
+ public async Task ReadLinesAsync_EmptyReader_ReturnsEmpty()
+ {
+ using var reader = new StringReader("");
+ var lines = await reader.ReadLinesAsync().ToListAsync();
+ Assert.Empty(lines);
+ }
+
+ [Fact]
+ public async Task ReadLinesAsync_SingleLine_ReturnsSingleLine()
+ {
+ using var reader = new StringReader("hello");
+ var lines = await reader.ReadLinesAsync().ToListAsync();
+ Assert.Single(lines, "hello");
+ }
+}
diff --git a/PowerKit.Tests/TimeSpanExtensionsTests.cs b/PowerKit.Tests/TimeSpanExtensionsTests.cs
new file mode 100644
index 0000000..5e282dd
--- /dev/null
+++ b/PowerKit.Tests/TimeSpanExtensionsTests.cs
@@ -0,0 +1,46 @@
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class TimeSpanExtensionsTests
+{
+ [Fact]
+ public void Clamp_ValueWithinRange_ReturnsValue()
+ {
+ var value = TimeSpan.FromSeconds(5);
+ var result = value.Clamp(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
+ Assert.Equal(value, result);
+ }
+
+ [Fact]
+ public void Clamp_ValueBelowMin_ReturnsMin()
+ {
+ var min = TimeSpan.FromSeconds(3);
+ var result = TimeSpan.FromSeconds(1).Clamp(min, TimeSpan.FromSeconds(10));
+ Assert.Equal(min, result);
+ }
+
+ [Fact]
+ public void Clamp_ValueAboveMax_ReturnsMax()
+ {
+ var max = TimeSpan.FromSeconds(7);
+ var result = TimeSpan.FromSeconds(20).Clamp(TimeSpan.FromSeconds(1), max);
+ Assert.Equal(max, result);
+ }
+
+ [Fact]
+ public void Clamp_ValueEqualToMin_ReturnsMin()
+ {
+ var min = TimeSpan.FromSeconds(3);
+ var result = min.Clamp(min, TimeSpan.FromSeconds(10));
+ Assert.Equal(min, result);
+ }
+
+ [Fact]
+ public void Clamp_ValueEqualToMax_ReturnsMax()
+ {
+ var max = TimeSpan.FromSeconds(10);
+ var result = max.Clamp(TimeSpan.FromSeconds(1), max);
+ Assert.Equal(max, result);
+ }
+}
diff --git a/PowerKit.slnx b/PowerKit.slnx
index fbcbc92..e5c1412 100644
--- a/PowerKit.slnx
+++ b/PowerKit.slnx
@@ -8,4 +8,5 @@
+
From 6d9a8f0b6690ab3c6ef7bd47af3cd59f5ac597f5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 17:29:45 +0000
Subject: [PATCH 08/43] test: adopt PolyShim naming and assertion conventions
in test files
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/a47ab08b-63ac-4925-a52c-3bc1d197288a
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
.../AggregateExceptionExtensionsTests.cs | 37 +++++--
.../AsyncEnumerableExtensionsTests.cs | 90 ++++++++++++----
PowerKit.Tests/DisposableTests.cs | 46 +++++---
PowerKit.Tests/EnumerableExtensionsTests.cs | 100 ++++++++++++------
PowerKit.Tests/ExceptionExtensionsTests.cs | 38 +++++--
PowerKit.Tests/FunctionalExtensionsTests.cs | 63 +++++++----
PowerKit.Tests/ObjectExtensionsTests.cs | 27 +++--
PowerKit.Tests/PathExtensionsTests.cs | 46 ++++----
PowerKit.Tests/PowerKit.Tests.csproj | 1 +
PowerKit.Tests/StreamExtensionsTests.cs | 41 ++++---
.../StringBuilderExtensionsTests.cs | 28 +++--
PowerKit.Tests/StringExtensionsTests.cs | 86 +++++++++------
PowerKit.Tests/TextReaderExtensionsTests.cs | 28 +++--
PowerKit.Tests/TimeSpanExtensionsTests.cs | 44 +++++---
14 files changed, 476 insertions(+), 199 deletions(-)
diff --git a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
index c63ff8d..dbad779 100644
--- a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
+++ b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -5,38 +6,58 @@ namespace PowerKit.Tests;
public class AggregateExceptionExtensionsTests
{
[Fact]
- public void TryGetSingle_SingleInnerException_ReturnsIt()
+ public void TryGetSingle_Test()
{
+ // Arrange
var inner = new Exception("only");
var aggregate = new AggregateException(inner);
- Assert.Same(inner, aggregate.TryGetSingle());
+ // Act
+ var result = aggregate.TryGetSingle();
+
+ // Assert
+ result.Should().BeSameAs(inner);
}
[Fact]
- public void TryGetSingle_MultipleInnerExceptions_ReturnsNull()
+ public void TryGetSingle_Multiple_Test()
{
+ // Arrange
var aggregate = new AggregateException(new Exception("a"), new Exception("b"));
- Assert.Null(aggregate.TryGetSingle());
+ // Act
+ var result = aggregate.TryGetSingle();
+
+ // Assert
+ result.Should().BeNull();
}
[Fact]
- public void TryGetSingle_NestedAggregateWithOneLeaf_ReturnsLeaf()
+ public void TryGetSingle_NestedSingleLeaf_Test()
{
+ // Arrange
var leaf = new Exception("leaf");
var nested = new AggregateException(leaf);
var outer = new AggregateException(nested);
- Assert.Same(leaf, outer.TryGetSingle());
+ // Act
+ var result = outer.TryGetSingle();
+
+ // Assert
+ result.Should().BeSameAs(leaf);
}
[Fact]
- public void TryGetSingle_NestedAggregateWithMultipleLeaves_ReturnsNull()
+ public void TryGetSingle_NestedMultipleLeaves_Test()
{
+ // Arrange
var nested = new AggregateException(new Exception("a"), new Exception("b"));
var outer = new AggregateException(nested);
- Assert.Null(outer.TryGetSingle());
+ // Act
+ var result = outer.TryGetSingle();
+
+ // Assert
+ result.Should().BeNull();
}
}
diff --git a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
index f2e8462..bfe24ae 100644
--- a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -11,40 +12,61 @@ private static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable sou
}
[Fact]
- public async Task TakeAsync_ZeroCount_ReturnsEmpty()
+ public async Task TakeAsync_Zero_Test()
{
+ // Arrange
var source = ToAsyncEnumerable([1, 2, 3]);
+
+ // Act
var result = await source.TakeAsync(0).ToListAsync();
- Assert.Empty(result);
+
+ // Assert
+ result.Should().BeEmpty();
}
[Fact]
- public async Task TakeAsync_NegativeCount_ReturnsEmpty()
+ public async Task TakeAsync_Negative_Test()
{
+ // Arrange
var source = ToAsyncEnumerable([1, 2, 3]);
+
+ // Act
var result = await source.TakeAsync(-1).ToListAsync();
- Assert.Empty(result);
+
+ // Assert
+ result.Should().BeEmpty();
}
[Fact]
- public async Task TakeAsync_CountLessThanSource_ReturnsThatMany()
+ public async Task TakeAsync_Test()
{
+ // Arrange
var source = ToAsyncEnumerable([1, 2, 3, 4, 5]);
+
+ // Act
var result = await source.TakeAsync(3).ToListAsync();
- Assert.Equal([1, 2, 3], result);
+
+ // Assert
+ result.Should().Equal(1, 2, 3);
}
[Fact]
- public async Task TakeAsync_CountGreaterThanSource_ReturnsAll()
+ public async Task TakeAsync_CountExceedsSource_Test()
{
+ // Arrange
var source = ToAsyncEnumerable([1, 2, 3]);
+
+ // Act
var result = await source.TakeAsync(10).ToListAsync();
- Assert.Equal([1, 2, 3], result);
+
+ // Assert
+ result.Should().Equal(1, 2, 3);
}
[Fact]
- public async Task TakeAsync_ZeroCount_DoesNotConsumeAnyElements()
+ public async Task TakeAsync_Zero_DoesNotConsumeElements_Test()
{
+ // Arrange
var consumed = 0;
async IAsyncEnumerable Tracked()
@@ -53,47 +75,75 @@ async IAsyncEnumerable Tracked()
yield return 1;
}
+ // Act
await Tracked().TakeAsync(0).ToListAsync();
- Assert.Equal(0, consumed);
+
+ // Assert
+ consumed.Should().Be(0);
}
[Fact]
- public async Task SelectManyAsync_FlattensResults()
+ public async Task SelectManyAsync_Test()
{
+ // Arrange
var source = ToAsyncEnumerable(["ab", "cd", "ef"]);
+
+ // Act
var result = await source.SelectManyAsync(s => s.ToCharArray()).ToListAsync();
- Assert.Equal(['a', 'b', 'c', 'd', 'e', 'f'], result);
+
+ // Assert
+ result.Should().Equal('a', 'b', 'c', 'd', 'e', 'f');
}
[Fact]
- public async Task SelectManyAsync_EmptySource_ReturnsEmpty()
+ public async Task SelectManyAsync_Empty_Test()
{
+ // Arrange
var source = ToAsyncEnumerable(Array.Empty());
+
+ // Act
var result = await source.SelectManyAsync(s => s.ToCharArray()).ToListAsync();
- Assert.Empty(result);
+
+ // Assert
+ result.Should().BeEmpty();
}
[Fact]
- public async Task ToListAsync_CollectsAllElements()
+ public async Task ToListAsync_Test()
{
+ // Arrange
var source = ToAsyncEnumerable([1, 2, 3]);
+
+ // Act
var result = await source.ToListAsync();
- Assert.Equal([1, 2, 3], result);
+
+ // Assert
+ result.Should().Equal(1, 2, 3);
}
[Fact]
- public async Task ToListAsync_EmptySource_ReturnsEmptyList()
+ public async Task ToListAsync_Empty_Test()
{
+ // Arrange
var source = ToAsyncEnumerable(Array.Empty());
+
+ // Act
var result = await source.ToListAsync();
- Assert.Empty(result);
+
+ // Assert
+ result.Should().BeEmpty();
}
[Fact]
- public async Task GetAwaiter_DirectAwait_ReturnsAllElements()
+ public async Task GetAwaiter_Test()
{
+ // Arrange
var source = ToAsyncEnumerable([10, 20, 30]);
+
+ // Act
var result = await source;
- Assert.Equal([10, 20, 30], result);
+
+ // Assert
+ result.Should().Equal(10, 20, 30);
}
}
diff --git a/PowerKit.Tests/DisposableTests.cs b/PowerKit.Tests/DisposableTests.cs
index 5643353..9665e8a 100644
--- a/PowerKit.Tests/DisposableTests.cs
+++ b/PowerKit.Tests/DisposableTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit;
namespace PowerKit.Tests;
@@ -5,49 +6,70 @@ namespace PowerKit.Tests;
public class DisposableTests
{
[Fact]
- public void Null_CanBeDisposedWithoutEffect()
+ public void Null_Test()
{
- // Should not throw
+ // Act & assert
Disposable.Null.Dispose();
}
[Fact]
- public void Create_InvokesActionOnDispose()
+ public void Create_Test()
{
+ // Arrange
var invoked = false;
- var disposable = Disposable.Create(() => invoked = true);
- Assert.False(invoked);
+ // Act
+ var disposable = Disposable.Create(() => invoked = true);
disposable.Dispose();
- Assert.True(invoked);
+
+ // Assert
+ invoked.Should().BeTrue();
}
[Fact]
- public void Merge_DisposesAllItems()
+ public void Create_NotDisposed_Test()
{
+ // Arrange
+ var invoked = false;
+
+ // Act
+ Disposable.Create(() => invoked = true);
+
+ // Assert
+ invoked.Should().BeFalse();
+ }
+
+ [Fact]
+ public void Merge_Test()
+ {
+ // Arrange
var count = 0;
var disposables = Enumerable
.Range(0, 3)
.Select(_ => Disposable.Create(() => count++))
.ToArray();
- var merged = Disposable.Merge(disposables);
- merged.Dispose();
+ // Act
+ Disposable.Merge(disposables).Dispose();
- Assert.Equal(3, count);
+ // Assert
+ count.Should().Be(3);
}
[Fact]
- public void Merge_DisposesInOrder()
+ public void Merge_Order_Test()
{
+ // Arrange
var order = new List();
var disposables = Enumerable
.Range(0, 3)
.Select(i => Disposable.Create(() => order.Add(i)))
.ToArray();
+ // Act
Disposable.Merge(disposables).Dispose();
- Assert.Equal([0, 1, 2], order);
+ // Assert
+ order.Should().Equal(0, 1, 2);
}
}
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
index b2aa0e3..9427a5d 100644
--- a/PowerKit.Tests/EnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -5,95 +6,130 @@ namespace PowerKit.Tests;
public class EnumerableExtensionsTests
{
[Fact]
- public void WhereNotNull_ReferenceType_FiltersNulls()
+ public void WhereNotNull_ReferenceType_Test()
{
+ // Arrange
string?[] source = ["a", null, "b", null, "c"];
- Assert.Equal(["a", "b", "c"], source.WhereNotNull());
+
+ // Act
+ var result = source.WhereNotNull();
+
+ // Assert
+ result.Should().Equal("a", "b", "c");
}
[Fact]
- public void WhereNotNull_ReferenceType_EmptySource_ReturnsEmpty()
+ public void WhereNotNull_ReferenceType_Empty_Test()
{
- Assert.Empty(Array.Empty().WhereNotNull());
+ // Act & assert
+ Array.Empty().WhereNotNull().Should().BeEmpty();
}
[Fact]
- public void WhereNotNull_NullableStruct_FiltersNulls()
+ public void WhereNotNull_NullableStruct_Test()
{
+ // Arrange
int?[] source = [1, null, 2, null, 3];
- Assert.Equal([1, 2, 3], source.WhereNotNull());
+
+ // Act
+ var result = source.WhereNotNull();
+
+ // Assert
+ result.Should().Equal(1, 2, 3);
}
[Fact]
- public void WhereNotNull_NullableStruct_EmptySource_ReturnsEmpty()
+ public void WhereNotNull_NullableStruct_Empty_Test()
{
- Assert.Empty(Array.Empty().WhereNotNull());
+ // Act & assert
+ Array.Empty().WhereNotNull().Should().BeEmpty();
}
[Fact]
- public void WhereNotNullOrWhiteSpace_FiltersNullsAndWhitespace()
+ public void WhereNotNullOrWhiteSpace_Test()
{
+ // Arrange
string?[] source = ["hello", null, " ", "", "world"];
- Assert.Equal(["hello", "world"], source.WhereNotNullOrWhiteSpace());
+
+ // Act
+ var result = source.WhereNotNullOrWhiteSpace();
+
+ // Assert
+ result.Should().Equal("hello", "world");
}
[Fact]
- public void WhereNotNullOrWhiteSpace_EmptySource_ReturnsEmpty()
+ public void WhereNotNullOrWhiteSpace_Empty_Test()
{
- Assert.Empty(Array.Empty().WhereNotNullOrWhiteSpace());
+ // Act & assert
+ Array.Empty().WhereNotNullOrWhiteSpace().Should().BeEmpty();
}
[Fact]
- public void FirstOrNull_NonEmptySource_ReturnsFirst()
+ public void FirstOrNull_Test()
{
+ // Arrange
int[] source = [5, 10, 15];
- Assert.Equal(5, source.FirstOrNull());
+
+ // Act
+ var result = source.FirstOrNull();
+
+ // Assert
+ result.Should().Be(5);
}
[Fact]
- public void FirstOrNull_EmptySource_ReturnsNull()
+ public void FirstOrNull_Empty_Test()
{
- Assert.Null(Array.Empty().FirstOrNull());
+ // Act & assert
+ Array.Empty().FirstOrNull().Should().BeNull();
}
[Fact]
- public void ElementAtOrNull_ValidIndex_ReturnsElement()
+ public void ElementAtOrNull_Test()
{
+ // Arrange
int[] source = [10, 20, 30];
- Assert.Equal(20, source.ElementAtOrNull(1));
+
+ // Act
+ var result = source.ElementAtOrNull(1);
+
+ // Assert
+ result.Should().Be(20);
}
[Fact]
- public void ElementAtOrNull_IndexZero_ReturnsFirst()
+ public void ElementAtOrNull_First_Test()
{
- int[] source = [42, 99];
- Assert.Equal(42, source.ElementAtOrNull(0));
+ // Act & assert
+ new[] { 42, 99 }.ElementAtOrNull(0).Should().Be(42);
}
[Fact]
- public void ElementAtOrNull_IndexAtLastElement_ReturnsLast()
+ public void ElementAtOrNull_Last_Test()
{
- int[] source = [1, 2, 3];
- Assert.Equal(3, source.ElementAtOrNull(2));
+ // Act & assert
+ new[] { 1, 2, 3 }.ElementAtOrNull(2).Should().Be(3);
}
[Fact]
- public void ElementAtOrNull_IndexOutOfRange_ReturnsNull()
+ public void ElementAtOrNull_OutOfRange_Test()
{
- int[] source = [1, 2, 3];
- Assert.Null(source.ElementAtOrNull(10));
+ // Act & assert
+ new[] { 1, 2, 3 }.ElementAtOrNull(10).Should().BeNull();
}
[Fact]
- public void ElementAtOrNull_NegativeIndex_ReturnsNull()
+ public void ElementAtOrNull_NegativeIndex_Test()
{
- int[] source = [1, 2, 3];
- Assert.Null(source.ElementAtOrNull(-1));
+ // Act & assert
+ new[] { 1, 2, 3 }.ElementAtOrNull(-1).Should().BeNull();
}
[Fact]
- public void ElementAtOrNull_EmptySource_ReturnsNull()
+ public void ElementAtOrNull_Empty_Test()
{
- Assert.Null(Array.Empty().ElementAtOrNull(0));
+ // Act & assert
+ Array.Empty().ElementAtOrNull(0).Should().BeNull();
}
}
diff --git a/PowerKit.Tests/ExceptionExtensionsTests.cs b/PowerKit.Tests/ExceptionExtensionsTests.cs
index b3be599..46888fe 100644
--- a/PowerKit.Tests/ExceptionExtensionsTests.cs
+++ b/PowerKit.Tests/ExceptionExtensionsTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -5,57 +6,74 @@ namespace PowerKit.Tests;
public class ExceptionExtensionsTests
{
[Fact]
- public void GetSelfAndDescendants_NoInner_ReturnsSelf()
+ public void GetSelfAndDescendants_NoInner_Test()
{
+ // Arrange
var ex = new Exception("root");
+
+ // Act
var result = ex.GetSelfAndDescendants();
- Assert.Single(result, ex);
+
+ // Assert
+ result.Should().Equal(ex);
}
[Fact]
- public void GetSelfAndDescendants_WithInnerException_ReturnsSelfAndInner()
+ public void GetSelfAndDescendants_WithInner_Test()
{
+ // Arrange
var inner = new Exception("inner");
var outer = new Exception("outer", inner);
+ // Act
var result = outer.GetSelfAndDescendants();
- Assert.Equal([outer, inner], result);
+ // Assert
+ result.Should().Equal(outer, inner);
}
[Fact]
- public void GetSelfAndDescendants_WithChainedInnerExceptions_ReturnsAll()
+ public void GetSelfAndDescendants_Chained_Test()
{
+ // Arrange
var leaf = new Exception("leaf");
var middle = new Exception("middle", leaf);
var root = new Exception("root", middle);
+ // Act
var result = root.GetSelfAndDescendants();
- Assert.Equal([root, middle, leaf], result);
+ // Assert
+ result.Should().Equal(root, middle, leaf);
}
[Fact]
- public void GetSelfAndDescendants_WithAggregateException_FlattensInnerExceptions()
+ public void GetSelfAndDescendants_Aggregate_Test()
{
+ // Arrange
var inner1 = new Exception("inner1");
var inner2 = new Exception("inner2");
var aggregate = new AggregateException("aggregate", inner1, inner2);
+ // Act
var result = aggregate.GetSelfAndDescendants();
- Assert.Equal([aggregate, inner1, inner2], result);
+ // Assert
+ result.Should().Equal(aggregate, inner1, inner2);
}
[Fact]
- public void GetSelfAndDescendants_NestedAggregateException_ReturnsAllDescendants()
+ public void GetSelfAndDescendants_NestedAggregate_Test()
{
+ // Arrange
var leaf = new Exception("leaf");
var inner = new AggregateException("inner", leaf);
var root = new AggregateException("root", inner);
+ // Act
var result = root.GetSelfAndDescendants();
- Assert.Equal([root, inner, leaf], result);
+ // Assert
+ result.Should().Equal(root, inner, leaf);
}
}
diff --git a/PowerKit.Tests/FunctionalExtensionsTests.cs b/PowerKit.Tests/FunctionalExtensionsTests.cs
index 527c995..878d1d7 100644
--- a/PowerKit.Tests/FunctionalExtensionsTests.cs
+++ b/PowerKit.Tests/FunctionalExtensionsTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -5,57 +6,79 @@ namespace PowerKit.Tests;
public class FunctionalExtensionsTests
{
[Fact]
- public void Pipe_TransformsValue()
+ public void Pipe_Test()
{
+ // Act
var result = 5.Pipe(x => x * 2);
- Assert.Equal(10, result);
+
+ // Assert
+ result.Should().Be(10);
}
[Fact]
- public void Pipe_ChainedCalls_AppliesInOrder()
+ public void Pipe_Chained_Test()
{
+ // Act
var result = "hello".Pipe(s => s.ToUpper()).Pipe(s => s + "!");
- Assert.Equal("HELLO!", result);
+
+ // Assert
+ result.Should().Be("HELLO!");
}
[Fact]
- public void NullIf_PredicateMatches_ReturnsNull()
+ public void NullIf_PredicateMatches_Test()
{
- int value = 0;
- Assert.Null(value.NullIf(v => v == 0));
+ // Act
+ var result = 0.NullIf(v => v == 0);
+
+ // Assert
+ result.Should().BeNull();
}
[Fact]
- public void NullIf_PredicateDoesNotMatch_ReturnsValue()
+ public void NullIf_PredicateDoesNotMatch_Test()
{
- int value = 5;
- Assert.Equal(5, value.NullIf(v => v == 0));
+ // Act
+ var result = 5.NullIf(v => v == 0);
+
+ // Assert
+ result.Should().Be(5);
}
[Fact]
- public void NullIfDefault_DefaultValue_ReturnsNull()
+ public void NullIfDefault_Default_Test()
{
- int value = 0;
- Assert.Null(value.NullIfDefault());
+ // Act
+ var result = 0.NullIfDefault();
+
+ // Assert
+ result.Should().BeNull();
}
[Fact]
- public void NullIfDefault_NonDefaultValue_ReturnsValue()
+ public void NullIfDefault_NonDefault_Test()
{
- int value = 42;
- Assert.Equal(42, value.NullIfDefault());
+ // Act
+ var result = 42.NullIfDefault();
+
+ // Assert
+ result.Should().Be(42);
}
[Fact]
- public void NullIfDefault_DefaultGuid_ReturnsNull()
+ public void NullIfDefault_DefaultGuid_Test()
{
- Assert.Null(Guid.Empty.NullIfDefault());
+ // Act & assert
+ Guid.Empty.NullIfDefault().Should().BeNull();
}
[Fact]
- public void NullIfDefault_NonDefaultGuid_ReturnsValue()
+ public void NullIfDefault_NonDefaultGuid_Test()
{
+ // Arrange
var id = Guid.NewGuid();
- Assert.Equal(id, id.NullIfDefault());
+
+ // Act & assert
+ id.NullIfDefault().Should().Be(id);
}
}
diff --git a/PowerKit.Tests/ObjectExtensionsTests.cs b/PowerKit.Tests/ObjectExtensionsTests.cs
index 7ac4128..c1958b7 100644
--- a/PowerKit.Tests/ObjectExtensionsTests.cs
+++ b/PowerKit.Tests/ObjectExtensionsTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -5,25 +6,35 @@ namespace PowerKit.Tests;
public class ObjectExtensionsTests
{
[Fact]
- public void ToSingletonEnumerable_ReturnsEnumerableWithSingleElement()
+ public void ToSingletonEnumerable_Test()
{
+ // Act
var result = 42.ToSingletonEnumerable().ToList();
- Assert.Single(result, 42);
+
+ // Assert
+ result.Should().Equal(42);
}
[Fact]
- public void ToSingletonEnumerable_WorksWithReferenceType()
+ public void ToSingletonEnumerable_ReferenceType_Test()
{
- var obj = "hello";
- var result = obj.ToSingletonEnumerable().ToList();
- Assert.Single(result, "hello");
+ // Act
+ var result = "hello".ToSingletonEnumerable().ToList();
+
+ // Assert
+ result.Should().Equal("hello");
}
[Fact]
- public void ToSingletonEnumerable_WorksWithNull()
+ public void ToSingletonEnumerable_Null_Test()
{
+ // Arrange
string? obj = null;
+
+ // Act
var result = obj.ToSingletonEnumerable().ToList();
- Assert.Single(result, (string?)null);
+
+ // Assert
+ result.Should().Equal((string?)null);
}
}
diff --git a/PowerKit.Tests/PathExtensionsTests.cs b/PowerKit.Tests/PathExtensionsTests.cs
index 7295c54..bcb307e 100644
--- a/PowerKit.Tests/PathExtensionsTests.cs
+++ b/PowerKit.Tests/PathExtensionsTests.cs
@@ -1,4 +1,5 @@
using System.IO;
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -6,56 +7,65 @@ namespace PowerKit.Tests;
public class PathExtensionsTests
{
[Fact]
- public void EscapeFileName_ValidName_ReturnsUnchanged()
+ public void EscapeFileName_Test()
{
- Assert.Equal("hello world.txt", Path.EscapeFileName("hello world.txt"));
+ // Act & assert
+ Path.EscapeFileName("hello world.txt").Should().Be("hello world.txt");
}
[Fact]
- public void EscapeFileName_ReplacesForwardSlash()
+ public void EscapeFileName_ForwardSlash_Test()
{
- Assert.Equal("a_b", Path.EscapeFileName("a/b"));
+ // Act & assert
+ Path.EscapeFileName("a/b").Should().Be("a_b");
}
[Fact]
- public void EscapeFileName_ReplacesBackslash()
+ public void EscapeFileName_Backslash_Test()
{
- Assert.Equal("a_b", Path.EscapeFileName("a\\b"));
+ // Act & assert
+ Path.EscapeFileName("a\\b").Should().Be("a_b");
}
[Fact]
- public void EscapeFileName_ReplacesColon()
+ public void EscapeFileName_Colon_Test()
{
- Assert.Equal("C_drive", Path.EscapeFileName("C:drive"));
+ // Act & assert
+ Path.EscapeFileName("C:drive").Should().Be("C_drive");
}
[Fact]
- public void EscapeFileName_ReplacesAllInvalidChars()
+ public void EscapeFileName_AllInvalidChars_Test()
{
- Assert.Equal("a_b_c_d_e_f_g_h_i", Path.EscapeFileName("a\0b/c\\d:e*f?g\"h
+
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index d93a95f..aa938f4 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -1,4 +1,5 @@
using System.IO;
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -6,56 +7,68 @@ namespace PowerKit.Tests;
public class StreamExtensionsTests
{
[Fact]
- public async Task CopyToAsync_WithAutoFlush_CopiesAllBytes()
+ public async Task CopyToAsync_AutoFlush_Test()
{
+ // Arrange
var data = new byte[] { 1, 2, 3, 4, 5 };
using var source = new MemoryStream(data);
using var destination = new MemoryStream();
+ // Act
await source.CopyToAsync(destination, autoFlush: true);
- Assert.Equal(data, destination.ToArray());
+ // Assert
+ destination.ToArray().Should().Equal(data);
}
[Fact]
- public async Task CopyToAsync_WithoutAutoFlush_CopiesAllBytes()
+ public async Task CopyToAsync_NoAutoFlush_Test()
{
+ // Arrange
var data = new byte[] { 10, 20, 30 };
using var source = new MemoryStream(data);
using var destination = new MemoryStream();
+ // Act
await source.CopyToAsync(destination, autoFlush: false);
- Assert.Equal(data, destination.ToArray());
+ // Assert
+ destination.ToArray().Should().Equal(data);
}
[Fact]
- public async Task CopyToAsync_EmptySource_ProducesEmptyDestination()
+ public async Task CopyToAsync_Empty_Test()
{
+ // Arrange
using var source = new MemoryStream();
using var destination = new MemoryStream();
+ // Act
await source.CopyToAsync(destination, autoFlush: false);
- Assert.Empty(destination.ToArray());
+ // Assert
+ destination.ToArray().Should().BeEmpty();
}
[Fact]
- public async Task CopyToAsync_WithProgress_CopiesAllBytes()
+ public async Task CopyToAsync_Progress_Test()
{
+ // Arrange
var data = new byte[1024];
- new Random(0).NextBytes(data);
using var source = new MemoryStream(data);
using var destination = new MemoryStream();
+ // Act
await source.CopyToAsync(destination, progress: null);
- Assert.Equal(data, destination.ToArray());
+ // Assert
+ destination.ToArray().Should().Equal(data);
}
[Fact]
- public async Task CopyToAsync_WithProgress_ReportsProgress()
+ public async Task CopyToAsync_Progress_Reports_Test()
{
+ // Arrange
var data = new byte[1024];
using var source = new MemoryStream(data);
using var destination = new MemoryStream();
@@ -63,13 +76,15 @@ public async Task CopyToAsync_WithProgress_ReportsProgress()
var reports = new List();
var progress = new Progress(v => reports.Add(v));
+ // Act
await source.CopyToAsync(destination, progress: progress);
// Allow Progress callbacks to fire on the thread pool
await Task.Delay(50);
- Assert.NotEmpty(reports);
- Assert.All(reports, v => Assert.InRange(v, 0.0, 1.0));
- Assert.Equal(1.0, reports[^1], precision: 5);
+ // Assert
+ reports.Should().NotBeEmpty();
+ reports.Should().AllSatisfy(v => v.Should().BeInRange(0.0, 1.0));
+ reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
}
}
diff --git a/PowerKit.Tests/StringBuilderExtensionsTests.cs b/PowerKit.Tests/StringBuilderExtensionsTests.cs
index abf3295..90a7b82 100644
--- a/PowerKit.Tests/StringBuilderExtensionsTests.cs
+++ b/PowerKit.Tests/StringBuilderExtensionsTests.cs
@@ -1,4 +1,5 @@
using System.Text;
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -6,26 +7,41 @@ namespace PowerKit.Tests;
public class StringBuilderExtensionsTests
{
[Fact]
- public void AppendIfNotEmpty_EmptyBuilder_DoesNotAppend()
+ public void AppendIfNotEmpty_Empty_Test()
{
+ // Arrange
var builder = new StringBuilder();
+
+ // Act
builder.AppendIfNotEmpty(',');
- Assert.Equal("", builder.ToString());
+
+ // Assert
+ builder.ToString().Should().Be("");
}
[Fact]
- public void AppendIfNotEmpty_NonEmptyBuilder_Appends()
+ public void AppendIfNotEmpty_Test()
{
+ // Arrange
var builder = new StringBuilder("hello");
+
+ // Act
builder.AppendIfNotEmpty(',');
- Assert.Equal("hello,", builder.ToString());
+
+ // Assert
+ builder.ToString().Should().Be("hello,");
}
[Fact]
- public void AppendIfNotEmpty_ReturnsBuilder_AllowsChaining()
+ public void AppendIfNotEmpty_Chaining_Test()
{
+ // Arrange
var builder = new StringBuilder("a");
+
+ // Act
builder.AppendIfNotEmpty(',').Append("b");
- Assert.Equal("a,b", builder.ToString());
+
+ // Assert
+ builder.ToString().Should().Be("a,b");
}
}
diff --git a/PowerKit.Tests/StringExtensionsTests.cs b/PowerKit.Tests/StringExtensionsTests.cs
index d3d4080..1047c6d 100644
--- a/PowerKit.Tests/StringExtensionsTests.cs
+++ b/PowerKit.Tests/StringExtensionsTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -5,104 +6,121 @@ namespace PowerKit.Tests;
public class StringExtensionsTests
{
[Fact]
- public void NullIfWhiteSpace_NonWhitespaceString_ReturnsSame()
+ public void NullIfWhiteSpace_Test()
{
- Assert.Equal("hello", "hello".NullIfWhiteSpace());
+ // Act & assert
+ "hello".NullIfWhiteSpace().Should().Be("hello");
}
[Fact]
- public void NullIfWhiteSpace_WhitespaceOnly_ReturnsNull()
+ public void NullIfWhiteSpace_Whitespace_Test()
{
- Assert.Null(" ".NullIfWhiteSpace());
+ // Act & assert
+ " ".NullIfWhiteSpace().Should().BeNull();
}
[Fact]
- public void NullIfWhiteSpace_EmptyString_ReturnsNull()
+ public void NullIfWhiteSpace_Empty_Test()
{
- Assert.Null("".NullIfWhiteSpace());
+ // Act & assert
+ "".NullIfWhiteSpace().Should().BeNull();
}
[Fact]
- public void SubstringUntil_SubstringFound_ReturnsBeforeIt()
+ public void SubstringUntil_Test()
{
- Assert.Equal("hello", "hello world".SubstringUntil(" "));
+ // Act & assert
+ "hello world".SubstringUntil(" ").Should().Be("hello");
}
[Fact]
- public void SubstringUntil_SubstringNotFound_ReturnsFullString()
+ public void SubstringUntil_NotFound_Test()
{
- Assert.Equal("hello", "hello".SubstringUntil("x"));
+ // Act & assert
+ "hello".SubstringUntil("x").Should().Be("hello");
}
[Fact]
- public void SubstringUntil_SubstringAtStart_ReturnsEmpty()
+ public void SubstringUntil_AtStart_Test()
{
- Assert.Equal("", "xhello".SubstringUntil("x"));
+ // Act & assert
+ "xhello".SubstringUntil("x").Should().Be("");
}
[Fact]
- public void SubstringAfter_SubstringFound_ReturnsAfterIt()
+ public void SubstringAfter_Test()
{
- Assert.Equal("world", "hello world".SubstringAfter(" "));
+ // Act & assert
+ "hello world".SubstringAfter(" ").Should().Be("world");
}
[Fact]
- public void SubstringAfter_SubstringNotFound_ReturnsEmpty()
+ public void SubstringAfter_NotFound_Test()
{
- Assert.Equal("", "hello".SubstringAfter("x"));
+ // Act & assert
+ "hello".SubstringAfter("x").Should().Be("");
}
[Fact]
- public void SubstringAfter_SubstringAtEnd_ReturnsEmpty()
+ public void SubstringAfter_AtEnd_Test()
{
- Assert.Equal("", "hellox".SubstringAfter("x"));
+ // Act & assert
+ "hellox".SubstringAfter("x").Should().Be("");
}
[Fact]
- public void Truncate_StringShorterThanLimit_ReturnsFull()
+ public void Truncate_ShorterThanLimit_Test()
{
- Assert.Equal("hi", "hi".Truncate(10));
+ // Act & assert
+ "hi".Truncate(10).Should().Be("hi");
}
[Fact]
- public void Truncate_StringExactlyAtLimit_ReturnsFull()
+ public void Truncate_ExactlyAtLimit_Test()
{
- Assert.Equal("hello", "hello".Truncate(5));
+ // Act & assert
+ "hello".Truncate(5).Should().Be("hello");
}
[Fact]
- public void Truncate_StringLongerThanLimit_ReturnsTruncated()
+ public void Truncate_LongerThanLimit_Test()
{
- Assert.Equal("hel", "hello".Truncate(3));
+ // Act & assert
+ "hello".Truncate(3).Should().Be("hel");
}
[Fact]
- public void ToSpaceSeparatedWords_PascalCase_InsertsSpacesBeforeUppercase()
+ public void ToSpaceSeparatedWords_Test()
{
- Assert.Equal("Hello World", "HelloWorld".ToSpaceSeparatedWords());
+ // Act & assert
+ "HelloWorld".ToSpaceSeparatedWords().Should().Be("Hello World");
}
[Fact]
- public void ToSpaceSeparatedWords_SingleWord_ReturnsUnchanged()
+ public void ToSpaceSeparatedWords_SingleWord_Test()
{
- Assert.Equal("Hello", "Hello".ToSpaceSeparatedWords());
+ // Act & assert
+ "Hello".ToSpaceSeparatedWords().Should().Be("Hello");
}
[Fact]
- public void ToSpaceSeparatedWords_AllLowercase_ReturnsUnchanged()
+ public void ToSpaceSeparatedWords_AllLowercase_Test()
{
- Assert.Equal("hello", "hello".ToSpaceSeparatedWords());
+ // Act & assert
+ "hello".ToSpaceSeparatedWords().Should().Be("hello");
}
[Fact]
- public void ToSpaceSeparatedWords_EmptyString_ReturnsEmpty()
+ public void ToSpaceSeparatedWords_Empty_Test()
{
- Assert.Equal("", "".ToSpaceSeparatedWords());
+ // Act & assert
+ "".ToSpaceSeparatedWords().Should().Be("");
}
[Fact]
- public void ToSpaceSeparatedWords_MultipleWords_SplitsCorrectly()
+ public void ToSpaceSeparatedWords_Multiple_Test()
{
- Assert.Equal("Foo Bar Baz", "FooBarBaz".ToSpaceSeparatedWords());
+ // Act & assert
+ "FooBarBaz".ToSpaceSeparatedWords().Should().Be("Foo Bar Baz");
}
}
diff --git a/PowerKit.Tests/TextReaderExtensionsTests.cs b/PowerKit.Tests/TextReaderExtensionsTests.cs
index 61d7e6b..4798e4d 100644
--- a/PowerKit.Tests/TextReaderExtensionsTests.cs
+++ b/PowerKit.Tests/TextReaderExtensionsTests.cs
@@ -1,4 +1,5 @@
using System.IO;
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -6,26 +7,41 @@ namespace PowerKit.Tests;
public class TextReaderExtensionsTests
{
[Fact]
- public async Task ReadLinesAsync_ReadsAllLines()
+ public async Task ReadLinesAsync_Test()
{
+ // Arrange
using var reader = new StringReader("line1\nline2\nline3");
+
+ // Act
var lines = await reader.ReadLinesAsync().ToListAsync();
- Assert.Equal(["line1", "line2", "line3"], lines);
+
+ // Assert
+ lines.Should().Equal("line1", "line2", "line3");
}
[Fact]
- public async Task ReadLinesAsync_EmptyReader_ReturnsEmpty()
+ public async Task ReadLinesAsync_Empty_Test()
{
+ // Arrange
using var reader = new StringReader("");
+
+ // Act
var lines = await reader.ReadLinesAsync().ToListAsync();
- Assert.Empty(lines);
+
+ // Assert
+ lines.Should().BeEmpty();
}
[Fact]
- public async Task ReadLinesAsync_SingleLine_ReturnsSingleLine()
+ public async Task ReadLinesAsync_SingleLine_Test()
{
+ // Arrange
using var reader = new StringReader("hello");
+
+ // Act
var lines = await reader.ReadLinesAsync().ToListAsync();
- Assert.Single(lines, "hello");
+
+ // Assert
+ lines.Should().Equal("hello");
}
}
diff --git a/PowerKit.Tests/TimeSpanExtensionsTests.cs b/PowerKit.Tests/TimeSpanExtensionsTests.cs
index 5e282dd..2e2be49 100644
--- a/PowerKit.Tests/TimeSpanExtensionsTests.cs
+++ b/PowerKit.Tests/TimeSpanExtensionsTests.cs
@@ -1,3 +1,4 @@
+using FluentAssertions;
using PowerKit.Extensions;
namespace PowerKit.Tests;
@@ -5,42 +6,61 @@ namespace PowerKit.Tests;
public class TimeSpanExtensionsTests
{
[Fact]
- public void Clamp_ValueWithinRange_ReturnsValue()
+ public void Clamp_Test()
{
+ // Arrange
var value = TimeSpan.FromSeconds(5);
+
+ // Act
var result = value.Clamp(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
- Assert.Equal(value, result);
+
+ // Assert
+ result.Should().Be(value);
}
[Fact]
- public void Clamp_ValueBelowMin_ReturnsMin()
+ public void Clamp_BelowMin_Test()
{
+ // Arrange
var min = TimeSpan.FromSeconds(3);
+
+ // Act
var result = TimeSpan.FromSeconds(1).Clamp(min, TimeSpan.FromSeconds(10));
- Assert.Equal(min, result);
+
+ // Assert
+ result.Should().Be(min);
}
[Fact]
- public void Clamp_ValueAboveMax_ReturnsMax()
+ public void Clamp_AboveMax_Test()
{
+ // Arrange
var max = TimeSpan.FromSeconds(7);
+
+ // Act
var result = TimeSpan.FromSeconds(20).Clamp(TimeSpan.FromSeconds(1), max);
- Assert.Equal(max, result);
+
+ // Assert
+ result.Should().Be(max);
}
[Fact]
- public void Clamp_ValueEqualToMin_ReturnsMin()
+ public void Clamp_EqualToMin_Test()
{
+ // Arrange
var min = TimeSpan.FromSeconds(3);
- var result = min.Clamp(min, TimeSpan.FromSeconds(10));
- Assert.Equal(min, result);
+
+ // Act & assert
+ min.Clamp(min, TimeSpan.FromSeconds(10)).Should().Be(min);
}
[Fact]
- public void Clamp_ValueEqualToMax_ReturnsMax()
+ public void Clamp_EqualToMax_Test()
{
+ // Arrange
var max = TimeSpan.FromSeconds(10);
- var result = max.Clamp(TimeSpan.FromSeconds(1), max);
- Assert.Equal(max, result);
+
+ // Act & assert
+ max.Clamp(TimeSpan.FromSeconds(1), max).Should().Be(max);
}
}
From ddd9770f395e23ca6e0b043a63f376b74dd6e36c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Apr 2026 19:55:29 +0000
Subject: [PATCH 09/43] Replace TimeSpanExtensions.Clamp with generic
ComparableExtensions.Clamp>
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/0fc311fa-8cb8-41a1-8ec3-9017eb211263
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/ComparableExtensionsTests.cs | 85 +++++++++++++++++++++
PowerKit.Tests/TimeSpanExtensionsTests.cs | 66 ----------------
PowerKit/Extensions/ComparableExtensions.cs | 20 +++++
PowerKit/Extensions/TimeSpanExtensions.cs | 20 -----
4 files changed, 105 insertions(+), 86 deletions(-)
create mode 100644 PowerKit.Tests/ComparableExtensionsTests.cs
delete mode 100644 PowerKit.Tests/TimeSpanExtensionsTests.cs
create mode 100644 PowerKit/Extensions/ComparableExtensions.cs
delete mode 100644 PowerKit/Extensions/TimeSpanExtensions.cs
diff --git a/PowerKit.Tests/ComparableExtensionsTests.cs b/PowerKit.Tests/ComparableExtensionsTests.cs
new file mode 100644
index 0000000..9bf166c
--- /dev/null
+++ b/PowerKit.Tests/ComparableExtensionsTests.cs
@@ -0,0 +1,85 @@
+using System;
+using FluentAssertions;
+using PowerKit.Extensions;
+
+namespace PowerKit.Tests;
+
+public class ComparableExtensionsTests
+{
+ [Fact]
+ public void Clamp_Test()
+ {
+ // Arrange & act
+ var result = 5.Clamp(1, 10);
+
+ // Assert
+ result.Should().Be(5);
+ }
+
+ [Fact]
+ public void Clamp_BelowMin_Test()
+ {
+ // Act
+ var result = 1.Clamp(3, 10);
+
+ // Assert
+ result.Should().Be(3);
+ }
+
+ [Fact]
+ public void Clamp_AboveMax_Test()
+ {
+ // Act
+ var result = 20.Clamp(1, 7);
+
+ // Assert
+ result.Should().Be(7);
+ }
+
+ [Fact]
+ public void Clamp_EqualToMin_Test()
+ {
+ // Act & assert
+ 3.Clamp(3, 10).Should().Be(3);
+ }
+
+ [Fact]
+ public void Clamp_EqualToMax_Test()
+ {
+ // Act & assert
+ 10.Clamp(1, 10).Should().Be(10);
+ }
+
+ [Fact]
+ public void Clamp_WithDouble_Test()
+ {
+ // Act
+ var result = 3.14.Clamp(0.0, 3.0);
+
+ // Assert
+ result.Should().Be(3.0);
+ }
+
+ [Fact]
+ public void Clamp_WithString_Test()
+ {
+ // Act
+ var result = "banana".Clamp("apple", "cherry");
+
+ // Assert
+ result.Should().Be("banana");
+ }
+
+ [Fact]
+ public void Clamp_WithTimeSpan_Test()
+ {
+ // Arrange
+ var value = TimeSpan.FromSeconds(5);
+
+ // Act
+ var result = value.Clamp(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
+
+ // Assert
+ result.Should().Be(value);
+ }
+}
diff --git a/PowerKit.Tests/TimeSpanExtensionsTests.cs b/PowerKit.Tests/TimeSpanExtensionsTests.cs
deleted file mode 100644
index 2e2be49..0000000
--- a/PowerKit.Tests/TimeSpanExtensionsTests.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using FluentAssertions;
-using PowerKit.Extensions;
-
-namespace PowerKit.Tests;
-
-public class TimeSpanExtensionsTests
-{
- [Fact]
- public void Clamp_Test()
- {
- // Arrange
- var value = TimeSpan.FromSeconds(5);
-
- // Act
- var result = value.Clamp(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
-
- // Assert
- result.Should().Be(value);
- }
-
- [Fact]
- public void Clamp_BelowMin_Test()
- {
- // Arrange
- var min = TimeSpan.FromSeconds(3);
-
- // Act
- var result = TimeSpan.FromSeconds(1).Clamp(min, TimeSpan.FromSeconds(10));
-
- // Assert
- result.Should().Be(min);
- }
-
- [Fact]
- public void Clamp_AboveMax_Test()
- {
- // Arrange
- var max = TimeSpan.FromSeconds(7);
-
- // Act
- var result = TimeSpan.FromSeconds(20).Clamp(TimeSpan.FromSeconds(1), max);
-
- // Assert
- result.Should().Be(max);
- }
-
- [Fact]
- public void Clamp_EqualToMin_Test()
- {
- // Arrange
- var min = TimeSpan.FromSeconds(3);
-
- // Act & assert
- min.Clamp(min, TimeSpan.FromSeconds(10)).Should().Be(min);
- }
-
- [Fact]
- public void Clamp_EqualToMax_Test()
- {
- // Arrange
- var max = TimeSpan.FromSeconds(10);
-
- // Act & assert
- max.Clamp(TimeSpan.FromSeconds(1), max).Should().Be(max);
- }
-}
diff --git a/PowerKit/Extensions/ComparableExtensions.cs b/PowerKit/Extensions/ComparableExtensions.cs
new file mode 100644
index 0000000..15bca7d
--- /dev/null
+++ b/PowerKit/Extensions/ComparableExtensions.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace PowerKit.Extensions;
+
+internal static class ComparableExtensions
+{
+ extension(T value) where T : IComparable
+ {
+ public T Clamp(T min, T max)
+ {
+ if (value.CompareTo(min) < 0)
+ return min;
+
+ if (value.CompareTo(max) > 0)
+ return max;
+
+ return value;
+ }
+ }
+}
diff --git a/PowerKit/Extensions/TimeSpanExtensions.cs b/PowerKit/Extensions/TimeSpanExtensions.cs
deleted file mode 100644
index 08e5a9d..0000000
--- a/PowerKit/Extensions/TimeSpanExtensions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-
-namespace PowerKit.Extensions;
-
-internal static class TimeSpanExtensions
-{
- extension(TimeSpan value)
- {
- public TimeSpan Clamp(TimeSpan min, TimeSpan max)
- {
- if (value < min)
- return min;
-
- if (value > max)
- return max;
-
- return value;
- }
- }
-}
From edfb1bdd067a132102f489cd9f674c7e5a3d0cd2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Apr 2026 19:58:30 +0000
Subject: [PATCH 10/43] Add GitHub Actions workflow (mirrors CliWrap main.yml)
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/8a790f45-7bdb-49da-9a53-dd78d357ea9d
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
.github/workflows/main.yml | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
create mode 100644 .github/workflows/main.yml
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..ea94d51
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,33 @@
+name: main
+
+on:
+ workflow_dispatch:
+ inputs:
+ package-version:
+ type: string
+ description: Package version
+ required: false
+ deploy:
+ type: boolean
+ description: Deploy package
+ required: false
+ default: false
+ push:
+ branches:
+ - prime
+ tags:
+ - "*"
+ pull_request:
+ branches:
+ - prime
+
+jobs:
+ main:
+ uses: Tyrrrz/.github/.github/workflows/nuget.yml@prime
+ with:
+ deploy: ${{ inputs.deploy || github.ref_type == 'tag' }}
+ package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }}
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
From 6f753480f0abeb88eb3fc1a13a7eaff0ea2944fe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Apr 2026 21:00:55 +0000
Subject: [PATCH 11/43] Update PolyShim to 2.8.0; mark
Microsoft.Bcl.AsyncInterfaces as PrivateAssets
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/a2f2d9ad-1a27-451f-b462-4d97ed31bd1e
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/PowerKit.csproj | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/PowerKit/PowerKit.csproj b/PowerKit/PowerKit.csproj
index 14d8aaf..5cb4b38 100644
--- a/PowerKit/PowerKit.csproj
+++ b/PowerKit/PowerKit.csproj
@@ -11,8 +11,8 @@
-
-
+
+
From 4fc36168991dc96bac6780cac49c693c5d800d0a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 11:54:55 +0000
Subject: [PATCH 12/43] Update PolyShim to 2.8.1, keep
Microsoft.Bcl.AsyncInterfaces workaround
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/0ecc3861-8296-4ffe-a67c-c976391d2542
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/PowerKit.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/PowerKit/PowerKit.csproj b/PowerKit/PowerKit.csproj
index 5cb4b38..29f3087 100644
--- a/PowerKit/PowerKit.csproj
+++ b/PowerKit/PowerKit.csproj
@@ -11,7 +11,7 @@
-
+
From 476b3ca17a2f227796956599cedf9daf78928e00 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 13:56:22 +0000
Subject: [PATCH 13/43] Update PolyShim to 2.8.2, remove
Microsoft.Bcl.AsyncInterfaces workaround
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/73f0b6ce-379f-44d9-b6ad-59549bf78ef3
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/PowerKit.csproj | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/PowerKit/PowerKit.csproj b/PowerKit/PowerKit.csproj
index 29f3087..f51c117 100644
--- a/PowerKit/PowerKit.csproj
+++ b/PowerKit/PowerKit.csproj
@@ -11,8 +11,7 @@
-
-
+
From efa44ba76c921f2eeea55706a3f09a6ae2eb654b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 17:31:43 +0000
Subject: [PATCH 14/43] Replace FQNs with using statements in
AsyncEnumerableExtensions and StringExtensions
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/567df8bb-d8b0-4e0a-b838-db698a6e0427
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/Extensions/AsyncEnumerableExtensions.cs | 3 ++-
PowerKit/Extensions/StringExtensions.cs | 5 +++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/PowerKit/Extensions/AsyncEnumerableExtensions.cs b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
index a6764bb..c4e78db 100644
--- a/PowerKit/Extensions/AsyncEnumerableExtensions.cs
+++ b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -34,7 +35,7 @@ var item in source
}
public async IAsyncEnumerable SelectManyAsync(
- System.Func> transform,
+ Func> transform,
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
diff --git a/PowerKit/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs
index 57ef073..50f0828 100644
--- a/PowerKit/Extensions/StringExtensions.cs
+++ b/PowerKit/Extensions/StringExtensions.cs
@@ -1,3 +1,4 @@
+using System;
using System.Text;
namespace PowerKit.Extensions;
@@ -10,7 +11,7 @@ internal static class StringExtensions
public string SubstringUntil(
string sub,
- System.StringComparison comparison = System.StringComparison.Ordinal
+ StringComparison comparison = StringComparison.Ordinal
) =>
str.IndexOf(sub, comparison) switch
{
@@ -20,7 +21,7 @@ public string SubstringUntil(
public string SubstringAfter(
string sub,
- System.StringComparison comparison = System.StringComparison.Ordinal
+ StringComparison comparison = StringComparison.Ordinal
) =>
str.IndexOf(sub, comparison) switch
{
From a18e3877dc1d135ba87fa72b4414e3f7c8ddb889 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 17:34:22 +0000
Subject: [PATCH 15/43] Add braces in AsyncEnumerableExtensions; add Min/Max to
ComparableExtensions with tests
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/970aa053-5479-4e79-a2e9-0a8055b320f6
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/ComparableExtensionsTests.cs | 42 +++++++++++++++++++
.../Extensions/AsyncEnumerableExtensions.cs | 4 ++
PowerKit/Extensions/ComparableExtensions.cs | 4 ++
3 files changed, 50 insertions(+)
diff --git a/PowerKit.Tests/ComparableExtensionsTests.cs b/PowerKit.Tests/ComparableExtensionsTests.cs
index 9bf166c..9cfba61 100644
--- a/PowerKit.Tests/ComparableExtensionsTests.cs
+++ b/PowerKit.Tests/ComparableExtensionsTests.cs
@@ -82,4 +82,46 @@ public void Clamp_WithTimeSpan_Test()
// Assert
result.Should().Be(value);
}
+
+ [Fact]
+ public void Min_ReturnsSmaller_Test()
+ {
+ // Act & assert
+ 5.Min(3).Should().Be(3);
+ }
+
+ [Fact]
+ public void Min_ReturnsValue_WhenSmaller_Test()
+ {
+ // Act & assert
+ 2.Min(7).Should().Be(2);
+ }
+
+ [Fact]
+ public void Min_ReturnsEither_WhenEqual_Test()
+ {
+ // Act & assert
+ 4.Min(4).Should().Be(4);
+ }
+
+ [Fact]
+ public void Max_ReturnsLarger_Test()
+ {
+ // Act & assert
+ 5.Max(3).Should().Be(5);
+ }
+
+ [Fact]
+ public void Max_ReturnsOther_WhenLarger_Test()
+ {
+ // Act & assert
+ 2.Max(7).Should().Be(7);
+ }
+
+ [Fact]
+ public void Max_ReturnsEither_WhenEqual_Test()
+ {
+ // Act & assert
+ 4.Max(4).Should().Be(4);
+ }
}
diff --git a/PowerKit/Extensions/AsyncEnumerableExtensions.cs b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
index c4e78db..f91ed0e 100644
--- a/PowerKit/Extensions/AsyncEnumerableExtensions.cs
+++ b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
@@ -44,8 +44,10 @@ var item in source
.WithCancellation(cancellationToken)
.ConfigureAwait(false)
)
+ {
foreach (var result in transform(item))
yield return result;
+ }
}
public async ValueTask> ToListAsync(
@@ -59,7 +61,9 @@ var item in source
.WithCancellation(cancellationToken)
.ConfigureAwait(false)
)
+ {
list.Add(item);
+ }
return list;
}
diff --git a/PowerKit/Extensions/ComparableExtensions.cs b/PowerKit/Extensions/ComparableExtensions.cs
index 15bca7d..b8e0639 100644
--- a/PowerKit/Extensions/ComparableExtensions.cs
+++ b/PowerKit/Extensions/ComparableExtensions.cs
@@ -16,5 +16,9 @@ public T Clamp(T min, T max)
return value;
}
+
+ public T Min(T other) => value.CompareTo(other) <= 0 ? value : other;
+
+ public T Max(T other) => value.CompareTo(other) >= 0 ? value : other;
}
}
From 5e4248922a93e611ff33a43e2e9e31277f05626c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 17:40:54 +0000
Subject: [PATCH 16/43] Add LastOrNull, NullIfEmpty/NullIfWhiteSpace for
strings, Trim for StringBuilder, CopyToAsync with contentLength, strip
trailing whitespace in EscapeFileName, move local fn to top
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/73499376-76d9-4185-8bf8-2691bd607f26
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/EnumerableExtensionsTests.cs | 27 ++++++
PowerKit.Tests/FunctionalExtensionsTests.cs | 42 +++++++++
PowerKit.Tests/PathExtensionsTests.cs | 14 +++
PowerKit.Tests/StreamExtensionsTests.cs | 38 ++++++++
.../StringBuilderExtensionsTests.cs | 91 +++++++++++++++++++
PowerKit.Tests/StringExtensionsTests.cs | 21 -----
PowerKit/Extensions/EnumerableExtensions.cs | 14 +++
PowerKit/Extensions/ExceptionExtensions.cs | 8 +-
PowerKit/Extensions/FunctionalExtensions.cs | 7 ++
PowerKit/Extensions/PathExtensions.cs | 6 +-
PowerKit/Extensions/StreamExtensions.cs | 39 ++++++++
.../Extensions/StringBuilderExtensions.cs | 15 +++
PowerKit/Extensions/StringExtensions.cs | 2 -
13 files changed, 295 insertions(+), 29 deletions(-)
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
index 9427a5d..3952199 100644
--- a/PowerKit.Tests/EnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -132,4 +132,31 @@ public void ElementAtOrNull_Empty_Test()
// Act & assert
Array.Empty().ElementAtOrNull(0).Should().BeNull();
}
+
+ [Fact]
+ public void LastOrNull_Test()
+ {
+ // Arrange
+ int[] source = [5, 10, 15];
+
+ // Act
+ var result = source.LastOrNull();
+
+ // Assert
+ result.Should().Be(15);
+ }
+
+ [Fact]
+ public void LastOrNull_Single_Test()
+ {
+ // Act & assert
+ new[] { 42 }.LastOrNull().Should().Be(42);
+ }
+
+ [Fact]
+ public void LastOrNull_Empty_Test()
+ {
+ // Act & assert
+ Array.Empty().LastOrNull().Should().BeNull();
+ }
}
diff --git a/PowerKit.Tests/FunctionalExtensionsTests.cs b/PowerKit.Tests/FunctionalExtensionsTests.cs
index 878d1d7..aed4c8d 100644
--- a/PowerKit.Tests/FunctionalExtensionsTests.cs
+++ b/PowerKit.Tests/FunctionalExtensionsTests.cs
@@ -81,4 +81,46 @@ public void NullIfDefault_NonDefaultGuid_Test()
// Act & assert
id.NullIfDefault().Should().Be(id);
}
+
+ [Fact]
+ public void NullIfEmpty_Test()
+ {
+ // Act & assert
+ "hello".NullIfEmpty().Should().Be("hello");
+ }
+
+ [Fact]
+ public void NullIfEmpty_Empty_Test()
+ {
+ // Act & assert
+ "".NullIfEmpty().Should().BeNull();
+ }
+
+ [Fact]
+ public void NullIfEmpty_Whitespace_Test()
+ {
+ // Act & assert
+ " ".NullIfEmpty().Should().Be(" ");
+ }
+
+ [Fact]
+ public void NullIfWhiteSpace_Test()
+ {
+ // Act & assert
+ "hello".NullIfWhiteSpace().Should().Be("hello");
+ }
+
+ [Fact]
+ public void NullIfWhiteSpace_Whitespace_Test()
+ {
+ // Act & assert
+ " ".NullIfWhiteSpace().Should().BeNull();
+ }
+
+ [Fact]
+ public void NullIfWhiteSpace_Empty_Test()
+ {
+ // Act & assert
+ "".NullIfWhiteSpace().Should().BeNull();
+ }
}
diff --git a/PowerKit.Tests/PathExtensionsTests.cs b/PowerKit.Tests/PathExtensionsTests.cs
index bcb307e..c73f125 100644
--- a/PowerKit.Tests/PathExtensionsTests.cs
+++ b/PowerKit.Tests/PathExtensionsTests.cs
@@ -48,6 +48,20 @@ public void EscapeFileName_TrailingDots_Test()
Path.EscapeFileName("hello...").Should().Be("hello");
}
+ [Fact]
+ public void EscapeFileName_TrailingWhitespace_Test()
+ {
+ // Act & assert
+ Path.EscapeFileName("hello ").Should().Be("hello");
+ }
+
+ [Fact]
+ public void EscapeFileName_TrailingDotsAndWhitespace_Test()
+ {
+ // Act & assert
+ Path.EscapeFileName("hello. . ").Should().Be("hello");
+ }
+
[Fact]
public void EscapeFileName_DotsInMiddle_Test()
{
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index aa938f4..203a4d4 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -87,4 +87,42 @@ public async Task CopyToAsync_Progress_Reports_Test()
reports.Should().AllSatisfy(v => v.Should().BeInRange(0.0, 1.0));
reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
}
+
+ [Fact]
+ public async Task CopyToAsync_ContentLength_Test()
+ {
+ // Arrange
+ var data = new byte[1024];
+ using var source = new MemoryStream(data);
+ using var destination = new MemoryStream();
+
+ // Act
+ await source.CopyToAsync(destination, contentLength: 1024);
+
+ // Assert
+ destination.ToArray().Should().Equal(data);
+ }
+
+ [Fact]
+ public async Task CopyToAsync_ContentLength_Progress_Test()
+ {
+ // Arrange
+ var data = new byte[1024];
+ using var source = new MemoryStream(data);
+ using var destination = new MemoryStream();
+
+ var reports = new List();
+ var progress = new Progress(v => reports.Add(v));
+
+ // Act
+ await source.CopyToAsync(destination, contentLength: 1024, progress: progress);
+
+ // Allow Progress callbacks to fire on the thread pool
+ await Task.Delay(50);
+
+ // Assert
+ reports.Should().NotBeEmpty();
+ reports.Should().AllSatisfy(v => v.Should().BeInRange(0.0, 1.0));
+ reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
+ }
}
diff --git a/PowerKit.Tests/StringBuilderExtensionsTests.cs b/PowerKit.Tests/StringBuilderExtensionsTests.cs
index 90a7b82..e013d78 100644
--- a/PowerKit.Tests/StringBuilderExtensionsTests.cs
+++ b/PowerKit.Tests/StringBuilderExtensionsTests.cs
@@ -44,4 +44,95 @@ public void AppendIfNotEmpty_Chaining_Test()
// Assert
builder.ToString().Should().Be("a,b");
}
+
+ [Fact]
+ public void Trim_Test()
+ {
+ // Arrange
+ var builder = new StringBuilder(" hello ");
+
+ // Act
+ builder.Trim();
+
+ // Assert
+ builder.ToString().Should().Be("hello");
+ }
+
+ [Fact]
+ public void Trim_LeadingOnly_Test()
+ {
+ // Arrange
+ var builder = new StringBuilder(" hello");
+
+ // Act
+ builder.Trim();
+
+ // Assert
+ builder.ToString().Should().Be("hello");
+ }
+
+ [Fact]
+ public void Trim_TrailingOnly_Test()
+ {
+ // Arrange
+ var builder = new StringBuilder("hello ");
+
+ // Act
+ builder.Trim();
+
+ // Assert
+ builder.ToString().Should().Be("hello");
+ }
+
+ [Fact]
+ public void Trim_NoWhitespace_Test()
+ {
+ // Arrange
+ var builder = new StringBuilder("hello");
+
+ // Act
+ builder.Trim();
+
+ // Assert
+ builder.ToString().Should().Be("hello");
+ }
+
+ [Fact]
+ public void Trim_Empty_Test()
+ {
+ // Arrange
+ var builder = new StringBuilder();
+
+ // Act
+ builder.Trim();
+
+ // Assert
+ builder.ToString().Should().Be("");
+ }
+
+ [Fact]
+ public void Trim_OnlyWhitespace_Test()
+ {
+ // Arrange
+ var builder = new StringBuilder(" ");
+
+ // Act
+ builder.Trim();
+
+ // Assert
+ builder.ToString().Should().Be("");
+ }
+
+ [Fact]
+ public void Trim_Chaining_Test()
+ {
+ // Arrange
+ var builder = new StringBuilder(" hello ");
+
+ // Act
+ var result = builder.Trim().Append("!");
+
+ // Assert
+ result.ToString().Should().Be("hello!");
+ }
}
diff --git a/PowerKit.Tests/StringExtensionsTests.cs b/PowerKit.Tests/StringExtensionsTests.cs
index 1047c6d..17b5382 100644
--- a/PowerKit.Tests/StringExtensionsTests.cs
+++ b/PowerKit.Tests/StringExtensionsTests.cs
@@ -5,27 +5,6 @@ namespace PowerKit.Tests;
public class StringExtensionsTests
{
- [Fact]
- public void NullIfWhiteSpace_Test()
- {
- // Act & assert
- "hello".NullIfWhiteSpace().Should().Be("hello");
- }
-
- [Fact]
- public void NullIfWhiteSpace_Whitespace_Test()
- {
- // Act & assert
- " ".NullIfWhiteSpace().Should().BeNull();
- }
-
- [Fact]
- public void NullIfWhiteSpace_Empty_Test()
- {
- // Act & assert
- "".NullIfWhiteSpace().Should().BeNull();
- }
-
[Fact]
public void SubstringUntil_Test()
{
diff --git a/PowerKit/Extensions/EnumerableExtensions.cs b/PowerKit/Extensions/EnumerableExtensions.cs
index fa147c2..50f4cf9 100644
--- a/PowerKit/Extensions/EnumerableExtensions.cs
+++ b/PowerKit/Extensions/EnumerableExtensions.cs
@@ -49,11 +49,25 @@ public IEnumerable WhereNotNullOrWhiteSpace()
public T? FirstOrNull()
{
foreach (var item in source)
+ {
return item;
+ }
return null;
}
+ public T? LastOrNull()
+ {
+ var last = default(T?);
+
+ foreach (var item in source)
+ {
+ last = item;
+ }
+
+ return last;
+ }
+
public T? ElementAtOrNull(int index)
{
var list = source as IReadOnlyList ?? source.ToArray();
diff --git a/PowerKit/Extensions/ExceptionExtensions.cs b/PowerKit/Extensions/ExceptionExtensions.cs
index 2c98a95..ab721f7 100644
--- a/PowerKit/Extensions/ExceptionExtensions.cs
+++ b/PowerKit/Extensions/ExceptionExtensions.cs
@@ -9,10 +9,6 @@ internal static class ExceptionExtensions
{
public IReadOnlyList GetSelfAndDescendants()
{
- var result = new List { exception };
- PopulateDescendants(exception, result);
- return result;
-
static void PopulateDescendants(Exception ex, ICollection result)
{
if (ex is AggregateException aggregateException)
@@ -29,6 +25,10 @@ static void PopulateDescendants(Exception ex, ICollection result)
PopulateDescendants(ex.InnerException, result);
}
}
+
+ var result = new List { exception };
+ PopulateDescendants(exception, result);
+ return result;
}
}
}
diff --git a/PowerKit/Extensions/FunctionalExtensions.cs b/PowerKit/Extensions/FunctionalExtensions.cs
index 8b8d49d..5e40566 100644
--- a/PowerKit/Extensions/FunctionalExtensions.cs
+++ b/PowerKit/Extensions/FunctionalExtensions.cs
@@ -18,4 +18,11 @@ internal static class FunctionalExtensions
public T? NullIfDefault() =>
value.NullIf(v => EqualityComparer.Default.Equals(v, default));
}
+
+ extension(string value)
+ {
+ public string? NullIfEmpty() => !string.IsNullOrEmpty(value) ? value : null;
+
+ public string? NullIfWhiteSpace() => !string.IsNullOrWhiteSpace(value) ? value : null;
+ }
}
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index c962be3..cb73306 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -33,9 +33,11 @@ public static string EscapeFileName(string fileName)
foreach (var c in fileName)
buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
- // File names cannot end with a dot (invalid on Windows, ambiguous on other filesystems)
- while (buffer.Length > 0 && buffer[buffer.Length - 1] == '.')
+ // File names cannot end with a dot or whitespace (invalid on Windows, ambiguous on other filesystems)
+ while (buffer.Length > 0 && (buffer[buffer.Length - 1] == '.' || char.IsWhiteSpace(buffer[buffer.Length - 1])))
+ {
buffer.Remove(buffer.Length - 1, 1);
+ }
return buffer.ToString();
}
diff --git a/PowerKit/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs
index 6925eed..fb4c523 100644
--- a/PowerKit/Extensions/StreamExtensions.cs
+++ b/PowerKit/Extensions/StreamExtensions.cs
@@ -53,7 +53,9 @@ public async ValueTask CopyToAsync(
.ConfigureAwait(false);
if (bytesRead <= 0)
+ {
break;
+ }
await destination
.WriteAsync(buffer.Memory[..bytesRead], cancellationToken)
@@ -62,7 +64,44 @@ await destination
totalBytesRead += bytesRead;
if (progress is not null && source.CanSeek && source.Length > 0)
+ {
progress.Report(1.0 * totalBytesRead / source.Length);
+ }
+ }
+ }
+
+ public async ValueTask CopyToAsync(
+ Stream destination,
+ long contentLength,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ using var buffer = MemoryPool.Shared.Rent(81920);
+
+ var totalBytesRead = 0L;
+
+ while (true)
+ {
+ var bytesRead = await source
+ .ReadAsync(buffer.Memory, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (bytesRead <= 0)
+ {
+ break;
+ }
+
+ await destination
+ .WriteAsync(buffer.Memory[..bytesRead], cancellationToken)
+ .ConfigureAwait(false);
+
+ totalBytesRead += bytesRead;
+
+ if (progress is not null && contentLength > 0)
+ {
+ progress.Report(1.0 * totalBytesRead / contentLength);
+ }
}
}
}
diff --git a/PowerKit/Extensions/StringBuilderExtensions.cs b/PowerKit/Extensions/StringBuilderExtensions.cs
index 15ef10f..5abc708 100644
--- a/PowerKit/Extensions/StringBuilderExtensions.cs
+++ b/PowerKit/Extensions/StringBuilderExtensions.cs
@@ -8,5 +8,20 @@ internal static class StringBuilderExtensions
{
public StringBuilder AppendIfNotEmpty(char value) =>
builder.Length > 0 ? builder.Append(value) : builder;
+
+ public StringBuilder Trim()
+ {
+ while (builder.Length > 0 && char.IsWhiteSpace(builder[builder.Length - 1]))
+ {
+ builder.Remove(builder.Length - 1, 1);
+ }
+
+ while (builder.Length > 0 && char.IsWhiteSpace(builder[0]))
+ {
+ builder.Remove(0, 1);
+ }
+
+ return builder;
+ }
}
}
diff --git a/PowerKit/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs
index 50f0828..cbee95e 100644
--- a/PowerKit/Extensions/StringExtensions.cs
+++ b/PowerKit/Extensions/StringExtensions.cs
@@ -7,8 +7,6 @@ internal static class StringExtensions
{
extension(string str)
{
- public string? NullIfWhiteSpace() => !string.IsNullOrWhiteSpace(str) ? str : null;
-
public string SubstringUntil(
string sub,
StringComparison comparison = StringComparison.Ordinal
From 754cc9e04c275d84062619d8e09f531c673f714e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 17:43:26 +0000
Subject: [PATCH 17/43] Optimize LastOrNull with IReadOnlyList fast path and
StringBuilder.Trim with batch Remove
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/73499376-76d9-4185-8bf8-2691bd607f26
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/Extensions/EnumerableExtensions.cs | 5 +++++
.../Extensions/StringBuilderExtensions.cs | 20 +++++++++++++++----
2 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/PowerKit/Extensions/EnumerableExtensions.cs b/PowerKit/Extensions/EnumerableExtensions.cs
index 50f4cf9..8bdc534 100644
--- a/PowerKit/Extensions/EnumerableExtensions.cs
+++ b/PowerKit/Extensions/EnumerableExtensions.cs
@@ -58,6 +58,11 @@ public IEnumerable WhereNotNullOrWhiteSpace()
public T? LastOrNull()
{
+ if (source is IReadOnlyList list)
+ {
+ return list.Count > 0 ? list[list.Count - 1] : null;
+ }
+
var last = default(T?);
foreach (var item in source)
diff --git a/PowerKit/Extensions/StringBuilderExtensions.cs b/PowerKit/Extensions/StringBuilderExtensions.cs
index 5abc708..d8e73df 100644
--- a/PowerKit/Extensions/StringBuilderExtensions.cs
+++ b/PowerKit/Extensions/StringBuilderExtensions.cs
@@ -11,14 +11,26 @@ public StringBuilder AppendIfNotEmpty(char value) =>
public StringBuilder Trim()
{
- while (builder.Length > 0 && char.IsWhiteSpace(builder[builder.Length - 1]))
+ var start = 0;
+ while (start < builder.Length && char.IsWhiteSpace(builder[start]))
{
- builder.Remove(builder.Length - 1, 1);
+ start++;
}
- while (builder.Length > 0 && char.IsWhiteSpace(builder[0]))
+ var end = builder.Length - 1;
+ while (end >= start && char.IsWhiteSpace(builder[end]))
{
- builder.Remove(0, 1);
+ end--;
+ }
+
+ if (end < builder.Length - 1)
+ {
+ builder.Remove(end + 1, builder.Length - end - 1);
+ }
+
+ if (start > 0)
+ {
+ builder.Remove(0, start);
}
return builder;
From 92e15909eedb76149adc05f469215af6efcf2f31 Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Fri, 10 Apr 2026 20:46:24 +0300
Subject: [PATCH 18/43] Update Directory.Build.props
---
Directory.Build.props | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/Directory.Build.props b/Directory.Build.props
index 85c9b94..0822769 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -9,6 +9,17 @@
false
+
+
+ annotations
+
+
+
+
+ false
+ false
+
+
$(Company)
Collection of utilities and extensions for rapid .NET development
From 8569b057b0e2198ce14730c343c76eddf2c2ae22 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 17:47:00 +0000
Subject: [PATCH 19/43] Rename ToSpaceSeparatedWords to SeparateWords with char
separator parameter
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/5ea59e8c-39c4-4615-8297-1fef32a7b0da
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/StringExtensionsTests.cs | 27 ++++++++++++++++---------
PowerKit/Extensions/StringExtensions.cs | 6 ++++--
2 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/PowerKit.Tests/StringExtensionsTests.cs b/PowerKit.Tests/StringExtensionsTests.cs
index 17b5382..f09a439 100644
--- a/PowerKit.Tests/StringExtensionsTests.cs
+++ b/PowerKit.Tests/StringExtensionsTests.cs
@@ -69,37 +69,44 @@ public void Truncate_LongerThanLimit_Test()
}
[Fact]
- public void ToSpaceSeparatedWords_Test()
+ public void SeparateWords_Test()
{
// Act & assert
- "HelloWorld".ToSpaceSeparatedWords().Should().Be("Hello World");
+ "HelloWorld".SeparateWords(' ').Should().Be("Hello World");
}
[Fact]
- public void ToSpaceSeparatedWords_SingleWord_Test()
+ public void SeparateWords_SingleWord_Test()
{
// Act & assert
- "Hello".ToSpaceSeparatedWords().Should().Be("Hello");
+ "Hello".SeparateWords(' ').Should().Be("Hello");
}
[Fact]
- public void ToSpaceSeparatedWords_AllLowercase_Test()
+ public void SeparateWords_AllLowercase_Test()
{
// Act & assert
- "hello".ToSpaceSeparatedWords().Should().Be("hello");
+ "hello".SeparateWords(' ').Should().Be("hello");
}
[Fact]
- public void ToSpaceSeparatedWords_Empty_Test()
+ public void SeparateWords_Empty_Test()
{
// Act & assert
- "".ToSpaceSeparatedWords().Should().Be("");
+ "".SeparateWords(' ').Should().Be("");
}
[Fact]
- public void ToSpaceSeparatedWords_Multiple_Test()
+ public void SeparateWords_Multiple_Test()
{
// Act & assert
- "FooBarBaz".ToSpaceSeparatedWords().Should().Be("Foo Bar Baz");
+ "FooBarBaz".SeparateWords(' ').Should().Be("Foo Bar Baz");
+ }
+
+ [Fact]
+ public void SeparateWords_CustomSeparator_Test()
+ {
+ // Act & assert
+ "FooBarBaz".SeparateWords('-').Should().Be("Foo-Bar-Baz");
}
}
diff --git a/PowerKit/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs
index cbee95e..c002f63 100644
--- a/PowerKit/Extensions/StringExtensions.cs
+++ b/PowerKit/Extensions/StringExtensions.cs
@@ -29,14 +29,16 @@ public string SubstringAfter(
public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str;
- public string ToSpaceSeparatedWords()
+ public string SeparateWords(char separator)
{
var builder = new StringBuilder(str.Length * 2);
foreach (var c in str)
{
if (char.IsUpper(c) && builder.Length > 0)
- builder.Append(' ');
+ {
+ builder.Append(separator);
+ }
builder.Append(c);
}
From b1fd164ce4bfb8484fd67680b7742c364a020f26 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 17:52:09 +0000
Subject: [PATCH 20/43] Use central package management
(Directory.Packages.props), move package ItemGroup to bottom
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/567bbb33-4906-4cee-8422-b0f5270b7025
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
Directory.Packages.props | 14 ++++++++++++++
PowerKit.Tests/PowerKit.Tests.csproj | 10 +++++-----
PowerKit.slnx | 1 +
PowerKit/PowerKit.csproj | 8 ++++----
4 files changed, 24 insertions(+), 9 deletions(-)
create mode 100644 Directory.Packages.props
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..0afbb3b
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,14 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PowerKit.Tests/PowerKit.Tests.csproj b/PowerKit.Tests/PowerKit.Tests.csproj
index 02a3b9e..ccce6ce 100644
--- a/PowerKit.Tests/PowerKit.Tests.csproj
+++ b/PowerKit.Tests/PowerKit.Tests.csproj
@@ -13,11 +13,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/PowerKit.slnx b/PowerKit.slnx
index e5c1412..eb54d4e 100644
--- a/PowerKit.slnx
+++ b/PowerKit.slnx
@@ -1,6 +1,7 @@
+
diff --git a/PowerKit/PowerKit.csproj b/PowerKit/PowerKit.csproj
index f51c117..eddfde8 100644
--- a/PowerKit/PowerKit.csproj
+++ b/PowerKit/PowerKit.csproj
@@ -10,10 +10,6 @@
true
-
-
-
-
+
+
+
+
From 06b9baa4fc5f815badbe118dd47cc3cd57711f1f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 19:46:55 +0000
Subject: [PATCH 21/43] Add await Task.Yield() to ToAsyncEnumerable test helper
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/5912ae80-5064-4eb6-b559-a0432c8b031e
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/AsyncEnumerableExtensionsTests.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
index bfe24ae..a2bd3ec 100644
--- a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
@@ -8,7 +8,10 @@ public class AsyncEnumerableExtensionsTests
private static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable source)
{
foreach (var item in source)
+ {
+ await Task.Yield();
yield return item;
+ }
}
[Fact]
From 3e9a7a5e46590e1ed3cb97dd795adb2c7fd9919d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 19:53:23 +0000
Subject: [PATCH 22/43] Simplify tests: remove global usings, merge
AsyncEnumerable/Comparable tests
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/6e5b93bc-cbb8-46fa-8e04-346fa5fa9d37
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
.../AggregateExceptionExtensionsTests.cs | 1 +
.../AsyncEnumerableExtensionsTests.cs | 137 +++---------------
PowerKit.Tests/ComparableExtensionsTests.cs | 106 +-------------
PowerKit.Tests/DisposableTests.cs | 1 +
PowerKit.Tests/EnumerableExtensionsTests.cs | 1 +
PowerKit.Tests/ExceptionExtensionsTests.cs | 1 +
PowerKit.Tests/FunctionalExtensionsTests.cs | 1 +
PowerKit.Tests/ObjectExtensionsTests.cs | 1 +
PowerKit.Tests/PathExtensionsTests.cs | 1 +
PowerKit.Tests/PowerKit.Tests.csproj | 4 -
PowerKit.Tests/StreamExtensionsTests.cs | 1 +
.../StringBuilderExtensionsTests.cs | 1 +
PowerKit.Tests/StringExtensionsTests.cs | 1 +
PowerKit.Tests/TextReaderExtensionsTests.cs | 1 +
14 files changed, 42 insertions(+), 216 deletions(-)
diff --git a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
index dbad779..6aa5c3e 100644
--- a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
+++ b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
index a2bd3ec..d5e566e 100644
--- a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
@@ -14,139 +15,47 @@ private static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable sou
}
}
- [Fact]
- public async Task TakeAsync_Zero_Test()
- {
- // Arrange
- var source = ToAsyncEnumerable([1, 2, 3]);
-
- // Act
- var result = await source.TakeAsync(0).ToListAsync();
-
- // Assert
- result.Should().BeEmpty();
- }
-
- [Fact]
- public async Task TakeAsync_Negative_Test()
- {
- // Arrange
- var source = ToAsyncEnumerable([1, 2, 3]);
-
- // Act
- var result = await source.TakeAsync(-1).ToListAsync();
-
- // Assert
- result.Should().BeEmpty();
- }
-
[Fact]
public async Task TakeAsync_Test()
{
- // Arrange
- var source = ToAsyncEnumerable([1, 2, 3, 4, 5]);
+ // Act & assert
+ (await ToAsyncEnumerable([1, 2, 3, 4, 5]).TakeAsync(3).ToListAsync())
+ .Should()
+ .Equal(1, 2, 3);
- // Act
- var result = await source.TakeAsync(3).ToListAsync();
+ (await ToAsyncEnumerable([1, 2, 3]).TakeAsync(0).ToListAsync())
+ .Should()
+ .BeEmpty();
- // Assert
- result.Should().Equal(1, 2, 3);
- }
-
- [Fact]
- public async Task TakeAsync_CountExceedsSource_Test()
- {
- // Arrange
- var source = ToAsyncEnumerable([1, 2, 3]);
-
- // Act
- var result = await source.TakeAsync(10).ToListAsync();
-
- // Assert
- result.Should().Equal(1, 2, 3);
- }
-
- [Fact]
- public async Task TakeAsync_Zero_DoesNotConsumeElements_Test()
- {
- // Arrange
- var consumed = 0;
-
- async IAsyncEnumerable Tracked()
- {
- consumed++;
- yield return 1;
- }
-
- // Act
- await Tracked().TakeAsync(0).ToListAsync();
-
- // Assert
- consumed.Should().Be(0);
+ (await ToAsyncEnumerable([1, 2, 3]).TakeAsync(10).ToListAsync())
+ .Should()
+ .Equal(1, 2, 3);
}
[Fact]
public async Task SelectManyAsync_Test()
{
- // Arrange
- var source = ToAsyncEnumerable(["ab", "cd", "ef"]);
-
- // Act
- var result = await source.SelectManyAsync(s => s.ToCharArray()).ToListAsync();
-
- // Assert
- result.Should().Equal('a', 'b', 'c', 'd', 'e', 'f');
- }
-
- [Fact]
- public async Task SelectManyAsync_Empty_Test()
- {
- // Arrange
- var source = ToAsyncEnumerable(Array.Empty());
-
- // Act
- var result = await source.SelectManyAsync(s => s.ToCharArray()).ToListAsync();
-
- // Assert
- result.Should().BeEmpty();
+ // Act & assert
+ (await ToAsyncEnumerable(["ab", "cd"]).SelectManyAsync(s => s.ToCharArray()).ToListAsync())
+ .Should()
+ .Equal('a', 'b', 'c', 'd');
}
[Fact]
public async Task ToListAsync_Test()
{
- // Arrange
- var source = ToAsyncEnumerable([1, 2, 3]);
-
- // Act
- var result = await source.ToListAsync();
-
- // Assert
- result.Should().Equal(1, 2, 3);
- }
-
- [Fact]
- public async Task ToListAsync_Empty_Test()
- {
- // Arrange
- var source = ToAsyncEnumerable(Array.Empty());
-
- // Act
- var result = await source.ToListAsync();
-
- // Assert
- result.Should().BeEmpty();
+ // Act & assert
+ (await ToAsyncEnumerable([1, 2, 3]).ToListAsync())
+ .Should()
+ .Equal(1, 2, 3);
}
[Fact]
public async Task GetAwaiter_Test()
{
- // Arrange
- var source = ToAsyncEnumerable([10, 20, 30]);
-
- // Act
- var result = await source;
-
- // Assert
- result.Should().Equal(10, 20, 30);
+ // Act & assert
+ (await ToAsyncEnumerable([10, 20, 30]))
+ .Should()
+ .Equal(10, 20, 30);
}
}
diff --git a/PowerKit.Tests/ComparableExtensionsTests.cs b/PowerKit.Tests/ComparableExtensionsTests.cs
index 9cfba61..c66747c 100644
--- a/PowerKit.Tests/ComparableExtensionsTests.cs
+++ b/PowerKit.Tests/ComparableExtensionsTests.cs
@@ -1,6 +1,6 @@
-using System;
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
@@ -8,120 +8,30 @@ public class ComparableExtensionsTests
{
[Fact]
public void Clamp_Test()
- {
- // Arrange & act
- var result = 5.Clamp(1, 10);
-
- // Assert
- result.Should().Be(5);
- }
-
- [Fact]
- public void Clamp_BelowMin_Test()
- {
- // Act
- var result = 1.Clamp(3, 10);
-
- // Assert
- result.Should().Be(3);
- }
-
- [Fact]
- public void Clamp_AboveMax_Test()
- {
- // Act
- var result = 20.Clamp(1, 7);
-
- // Assert
- result.Should().Be(7);
- }
-
- [Fact]
- public void Clamp_EqualToMin_Test()
{
// Act & assert
- 3.Clamp(3, 10).Should().Be(3);
- }
-
- [Fact]
- public void Clamp_EqualToMax_Test()
- {
- // Act & assert
- 10.Clamp(1, 10).Should().Be(10);
- }
-
- [Fact]
- public void Clamp_WithDouble_Test()
- {
- // Act
- var result = 3.14.Clamp(0.0, 3.0);
-
- // Assert
- result.Should().Be(3.0);
- }
-
- [Fact]
- public void Clamp_WithString_Test()
- {
- // Act
- var result = "banana".Clamp("apple", "cherry");
-
- // Assert
- result.Should().Be("banana");
- }
-
- [Fact]
- public void Clamp_WithTimeSpan_Test()
- {
- // Arrange
- var value = TimeSpan.FromSeconds(5);
-
- // Act
- var result = value.Clamp(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
-
- // Assert
- result.Should().Be(value);
+ 5.Clamp(1, 10).Should().Be(5);
+ 1.Clamp(3, 10).Should().Be(3);
+ 20.Clamp(1, 7).Should().Be(7);
+ 3.14.Clamp(0.0, 3.0).Should().Be(3.0);
+ "banana".Clamp("apple", "cherry").Should().Be("banana");
}
[Fact]
- public void Min_ReturnsSmaller_Test()
+ public void Min_Test()
{
// Act & assert
5.Min(3).Should().Be(3);
- }
-
- [Fact]
- public void Min_ReturnsValue_WhenSmaller_Test()
- {
- // Act & assert
2.Min(7).Should().Be(2);
- }
-
- [Fact]
- public void Min_ReturnsEither_WhenEqual_Test()
- {
- // Act & assert
4.Min(4).Should().Be(4);
}
[Fact]
- public void Max_ReturnsLarger_Test()
+ public void Max_Test()
{
// Act & assert
5.Max(3).Should().Be(5);
- }
-
- [Fact]
- public void Max_ReturnsOther_WhenLarger_Test()
- {
- // Act & assert
2.Max(7).Should().Be(7);
- }
-
- [Fact]
- public void Max_ReturnsEither_WhenEqual_Test()
- {
- // Act & assert
4.Max(4).Should().Be(4);
}
}
diff --git a/PowerKit.Tests/DisposableTests.cs b/PowerKit.Tests/DisposableTests.cs
index 9665e8a..096fb8d 100644
--- a/PowerKit.Tests/DisposableTests.cs
+++ b/PowerKit.Tests/DisposableTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using PowerKit;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
index 3952199..22c5454 100644
--- a/PowerKit.Tests/EnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/ExceptionExtensionsTests.cs b/PowerKit.Tests/ExceptionExtensionsTests.cs
index 46888fe..e3bf08e 100644
--- a/PowerKit.Tests/ExceptionExtensionsTests.cs
+++ b/PowerKit.Tests/ExceptionExtensionsTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/FunctionalExtensionsTests.cs b/PowerKit.Tests/FunctionalExtensionsTests.cs
index aed4c8d..76f8e34 100644
--- a/PowerKit.Tests/FunctionalExtensionsTests.cs
+++ b/PowerKit.Tests/FunctionalExtensionsTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/ObjectExtensionsTests.cs b/PowerKit.Tests/ObjectExtensionsTests.cs
index c1958b7..68af34d 100644
--- a/PowerKit.Tests/ObjectExtensionsTests.cs
+++ b/PowerKit.Tests/ObjectExtensionsTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/PathExtensionsTests.cs b/PowerKit.Tests/PathExtensionsTests.cs
index c73f125..b34a8ef 100644
--- a/PowerKit.Tests/PathExtensionsTests.cs
+++ b/PowerKit.Tests/PathExtensionsTests.cs
@@ -1,6 +1,7 @@
using System.IO;
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/PowerKit.Tests.csproj b/PowerKit.Tests/PowerKit.Tests.csproj
index ccce6ce..98ac4b7 100644
--- a/PowerKit.Tests/PowerKit.Tests.csproj
+++ b/PowerKit.Tests/PowerKit.Tests.csproj
@@ -20,9 +20,5 @@
-
-
-
-
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index 203a4d4..a4966cf 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -1,6 +1,7 @@
using System.IO;
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/StringBuilderExtensionsTests.cs b/PowerKit.Tests/StringBuilderExtensionsTests.cs
index e013d78..adc81d8 100644
--- a/PowerKit.Tests/StringBuilderExtensionsTests.cs
+++ b/PowerKit.Tests/StringBuilderExtensionsTests.cs
@@ -1,6 +1,7 @@
using System.Text;
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/StringExtensionsTests.cs b/PowerKit.Tests/StringExtensionsTests.cs
index f09a439..eed8fca 100644
--- a/PowerKit.Tests/StringExtensionsTests.cs
+++ b/PowerKit.Tests/StringExtensionsTests.cs
@@ -1,5 +1,6 @@
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
diff --git a/PowerKit.Tests/TextReaderExtensionsTests.cs b/PowerKit.Tests/TextReaderExtensionsTests.cs
index 4798e4d..2a6e65d 100644
--- a/PowerKit.Tests/TextReaderExtensionsTests.cs
+++ b/PowerKit.Tests/TextReaderExtensionsTests.cs
@@ -1,6 +1,7 @@
using System.IO;
using FluentAssertions;
using PowerKit.Extensions;
+using Xunit;
namespace PowerKit.Tests;
From bd9c9b964aa81ec5eef810ffaa8c3305469e1023 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 20:04:48 +0000
Subject: [PATCH 23/43] Use .NET 10, remove ImplicitUsings, merge tests (1 per
implementation method)
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/9d5de743-75ab-4f65-8a3e-3bbf5b6e0a4a
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
.../AggregateExceptionExtensionsTests.cs | 77 ++++------
.../AsyncEnumerableExtensionsTests.cs | 2 +
PowerKit.Tests/DisposableTests.cs | 39 +----
PowerKit.Tests/EnumerableExtensionsTests.cs | 141 +++---------------
PowerKit.Tests/ExceptionExtensionsTests.cs | 102 +++++--------
PowerKit.Tests/FunctionalExtensionsTests.cs | 97 ++----------
PowerKit.Tests/ObjectExtensionsTests.cs | 33 +---
PowerKit.Tests/PathExtensionsTests.cs | 60 --------
PowerKit.Tests/PowerKit.Tests.csproj | 3 +-
PowerKit.Tests/StreamExtensionsTests.cs | 3 +
.../StringBuilderExtensionsTests.cs | 132 ++--------------
PowerKit.Tests/StringExtensionsTests.cs | 68 +--------
PowerKit.Tests/TextReaderExtensionsTests.cs | 47 ++----
13 files changed, 130 insertions(+), 674 deletions(-)
diff --git a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
index 6aa5c3e..9ad8752 100644
--- a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
+++ b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
@@ -1,3 +1,4 @@
+using System;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
@@ -9,56 +10,30 @@ public class AggregateExceptionExtensionsTests
[Fact]
public void TryGetSingle_Test()
{
- // Arrange
- var inner = new Exception("only");
- var aggregate = new AggregateException(inner);
-
- // Act
- var result = aggregate.TryGetSingle();
-
- // Assert
- result.Should().BeSameAs(inner);
- }
-
- [Fact]
- public void TryGetSingle_Multiple_Test()
- {
- // Arrange
- var aggregate = new AggregateException(new Exception("a"), new Exception("b"));
-
- // Act
- var result = aggregate.TryGetSingle();
-
- // Assert
- result.Should().BeNull();
- }
-
- [Fact]
- public void TryGetSingle_NestedSingleLeaf_Test()
- {
- // Arrange
- var leaf = new Exception("leaf");
- var nested = new AggregateException(leaf);
- var outer = new AggregateException(nested);
-
- // Act
- var result = outer.TryGetSingle();
-
- // Assert
- result.Should().BeSameAs(leaf);
- }
-
- [Fact]
- public void TryGetSingle_NestedMultipleLeaves_Test()
- {
- // Arrange
- var nested = new AggregateException(new Exception("a"), new Exception("b"));
- var outer = new AggregateException(nested);
-
- // Act
- var result = outer.TryGetSingle();
-
- // Assert
- result.Should().BeNull();
+ {
+ // Act & assert
+ var inner = new Exception("only");
+ new AggregateException(inner).TryGetSingle().Should().BeSameAs(inner);
+ }
+
+ new AggregateException(new Exception("a"), new Exception("b"))
+ .TryGetSingle()
+ .Should()
+ .BeNull();
+
+ {
+ var leaf = new Exception("leaf");
+ new AggregateException(new AggregateException(leaf))
+ .TryGetSingle()
+ .Should()
+ .BeSameAs(leaf);
+ }
+
+ new AggregateException(
+ new AggregateException(new Exception("a"), new Exception("b"))
+ )
+ .TryGetSingle()
+ .Should()
+ .BeNull();
}
}
diff --git a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
index d5e566e..fdba27c 100644
--- a/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/AsyncEnumerableExtensionsTests.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
diff --git a/PowerKit.Tests/DisposableTests.cs b/PowerKit.Tests/DisposableTests.cs
index 096fb8d..d76edcd 100644
--- a/PowerKit.Tests/DisposableTests.cs
+++ b/PowerKit.Tests/DisposableTests.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+using System.Linq;
using FluentAssertions;
using PowerKit;
using Xunit;
@@ -18,47 +20,16 @@ public void Create_Test()
{
// Arrange
var invoked = false;
-
- // Act
var disposable = Disposable.Create(() => invoked = true);
- disposable.Dispose();
-
- // Assert
- invoked.Should().BeTrue();
- }
-
- [Fact]
- public void Create_NotDisposed_Test()
- {
- // Arrange
- var invoked = false;
- // Act
- Disposable.Create(() => invoked = true);
-
- // Assert
+ // Act & assert
invoked.Should().BeFalse();
+ disposable.Dispose();
+ invoked.Should().BeTrue();
}
[Fact]
public void Merge_Test()
- {
- // Arrange
- var count = 0;
- var disposables = Enumerable
- .Range(0, 3)
- .Select(_ => Disposable.Create(() => count++))
- .ToArray();
-
- // Act
- Disposable.Merge(disposables).Dispose();
-
- // Assert
- count.Should().Be(3);
- }
-
- [Fact]
- public void Merge_Order_Test()
{
// Arrange
var order = new List();
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
index 22c5454..9330576 100644
--- a/PowerKit.Tests/EnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -1,3 +1,4 @@
+using System;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
@@ -7,157 +8,49 @@ namespace PowerKit.Tests;
public class EnumerableExtensionsTests
{
[Fact]
- public void WhereNotNull_ReferenceType_Test()
- {
- // Arrange
- string?[] source = ["a", null, "b", null, "c"];
-
- // Act
- var result = source.WhereNotNull();
-
- // Assert
- result.Should().Equal("a", "b", "c");
- }
-
- [Fact]
- public void WhereNotNull_ReferenceType_Empty_Test()
+ public void WhereNotNull_Test()
{
// Act & assert
+ new string?[] { "a", null, "b", null, "c" }.WhereNotNull().Should().Equal("a", "b", "c");
+ new int?[] { 1, null, 2, null, 3 }.WhereNotNull().Should().Equal(1, 2, 3);
Array.Empty().WhereNotNull().Should().BeEmpty();
}
- [Fact]
- public void WhereNotNull_NullableStruct_Test()
- {
- // Arrange
- int?[] source = [1, null, 2, null, 3];
-
- // Act
- var result = source.WhereNotNull();
-
- // Assert
- result.Should().Equal(1, 2, 3);
- }
-
- [Fact]
- public void WhereNotNull_NullableStruct_Empty_Test()
- {
- // Act & assert
- Array.Empty().WhereNotNull().Should().BeEmpty();
- }
-
[Fact]
public void WhereNotNullOrWhiteSpace_Test()
- {
- // Arrange
- string?[] source = ["hello", null, " ", "", "world"];
-
- // Act
- var result = source.WhereNotNullOrWhiteSpace();
-
- // Assert
- result.Should().Equal("hello", "world");
- }
-
- [Fact]
- public void WhereNotNullOrWhiteSpace_Empty_Test()
{
// Act & assert
- Array.Empty().WhereNotNullOrWhiteSpace().Should().BeEmpty();
+ new string?[] { "hello", null, " ", "", "world" }
+ .WhereNotNullOrWhiteSpace()
+ .Should()
+ .Equal("hello", "world");
}
[Fact]
public void FirstOrNull_Test()
- {
- // Arrange
- int[] source = [5, 10, 15];
-
- // Act
- var result = source.FirstOrNull();
-
- // Assert
- result.Should().Be(5);
- }
-
- [Fact]
- public void FirstOrNull_Empty_Test()
{
// Act & assert
+ new[] { 5, 10, 15 }.FirstOrNull().Should().Be(5);
Array.Empty().FirstOrNull().Should().BeNull();
}
- [Fact]
- public void ElementAtOrNull_Test()
- {
- // Arrange
- int[] source = [10, 20, 30];
-
- // Act
- var result = source.ElementAtOrNull(1);
-
- // Assert
- result.Should().Be(20);
- }
-
- [Fact]
- public void ElementAtOrNull_First_Test()
- {
- // Act & assert
- new[] { 42, 99 }.ElementAtOrNull(0).Should().Be(42);
- }
-
- [Fact]
- public void ElementAtOrNull_Last_Test()
- {
- // Act & assert
- new[] { 1, 2, 3 }.ElementAtOrNull(2).Should().Be(3);
- }
-
- [Fact]
- public void ElementAtOrNull_OutOfRange_Test()
- {
- // Act & assert
- new[] { 1, 2, 3 }.ElementAtOrNull(10).Should().BeNull();
- }
-
- [Fact]
- public void ElementAtOrNull_NegativeIndex_Test()
- {
- // Act & assert
- new[] { 1, 2, 3 }.ElementAtOrNull(-1).Should().BeNull();
- }
-
- [Fact]
- public void ElementAtOrNull_Empty_Test()
- {
- // Act & assert
- Array.Empty().ElementAtOrNull(0).Should().BeNull();
- }
-
[Fact]
public void LastOrNull_Test()
- {
- // Arrange
- int[] source = [5, 10, 15];
-
- // Act
- var result = source.LastOrNull();
-
- // Assert
- result.Should().Be(15);
- }
-
- [Fact]
- public void LastOrNull_Single_Test()
{
// Act & assert
+ new[] { 5, 10, 15 }.LastOrNull().Should().Be(15);
new[] { 42 }.LastOrNull().Should().Be(42);
+ Array.Empty().LastOrNull().Should().BeNull();
}
[Fact]
- public void LastOrNull_Empty_Test()
+ public void ElementAtOrNull_Test()
{
// Act & assert
- Array.Empty().LastOrNull().Should().BeNull();
+ new[] { 10, 20, 30 }.ElementAtOrNull(1).Should().Be(20);
+ new[] { 10, 20, 30 }.ElementAtOrNull(0).Should().Be(10);
+ new[] { 10, 20, 30 }.ElementAtOrNull(10).Should().BeNull();
+ new[] { 10, 20, 30 }.ElementAtOrNull(-1).Should().BeNull();
+ Array.Empty().ElementAtOrNull(0).Should().BeNull();
}
}
diff --git a/PowerKit.Tests/ExceptionExtensionsTests.cs b/PowerKit.Tests/ExceptionExtensionsTests.cs
index e3bf08e..01d70a0 100644
--- a/PowerKit.Tests/ExceptionExtensionsTests.cs
+++ b/PowerKit.Tests/ExceptionExtensionsTests.cs
@@ -1,3 +1,4 @@
+using System;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
@@ -7,74 +8,39 @@ namespace PowerKit.Tests;
public class ExceptionExtensionsTests
{
[Fact]
- public void GetSelfAndDescendants_NoInner_Test()
+ public void GetSelfAndDescendants_Test()
{
- // Arrange
- var ex = new Exception("root");
-
- // Act
- var result = ex.GetSelfAndDescendants();
-
- // Assert
- result.Should().Equal(ex);
- }
-
- [Fact]
- public void GetSelfAndDescendants_WithInner_Test()
- {
- // Arrange
- var inner = new Exception("inner");
- var outer = new Exception("outer", inner);
-
- // Act
- var result = outer.GetSelfAndDescendants();
-
- // Assert
- result.Should().Equal(outer, inner);
- }
-
- [Fact]
- public void GetSelfAndDescendants_Chained_Test()
- {
- // Arrange
- var leaf = new Exception("leaf");
- var middle = new Exception("middle", leaf);
- var root = new Exception("root", middle);
-
- // Act
- var result = root.GetSelfAndDescendants();
-
- // Assert
- result.Should().Equal(root, middle, leaf);
- }
-
- [Fact]
- public void GetSelfAndDescendants_Aggregate_Test()
- {
- // Arrange
- var inner1 = new Exception("inner1");
- var inner2 = new Exception("inner2");
- var aggregate = new AggregateException("aggregate", inner1, inner2);
-
- // Act
- var result = aggregate.GetSelfAndDescendants();
-
- // Assert
- result.Should().Equal(aggregate, inner1, inner2);
- }
-
- [Fact]
- public void GetSelfAndDescendants_NestedAggregate_Test()
- {
- // Arrange
- var leaf = new Exception("leaf");
- var inner = new AggregateException("inner", leaf);
- var root = new AggregateException("root", inner);
-
- // Act
- var result = root.GetSelfAndDescendants();
-
- // Assert
- result.Should().Equal(root, inner, leaf);
+ {
+ // Act & assert
+ var ex = new Exception("root");
+ ex.GetSelfAndDescendants().Should().Equal(ex);
+ }
+
+ {
+ var inner = new Exception("inner");
+ var outer = new Exception("outer", inner);
+ outer.GetSelfAndDescendants().Should().Equal(outer, inner);
+ }
+
+ {
+ var leaf = new Exception("leaf");
+ var middle = new Exception("middle", leaf);
+ var root = new Exception("root", middle);
+ root.GetSelfAndDescendants().Should().Equal(root, middle, leaf);
+ }
+
+ {
+ var inner1 = new Exception("inner1");
+ var inner2 = new Exception("inner2");
+ var aggregate = new AggregateException("aggregate", inner1, inner2);
+ aggregate.GetSelfAndDescendants().Should().Equal(aggregate, inner1, inner2);
+ }
+
+ {
+ var leaf = new Exception("leaf");
+ var inner = new AggregateException("inner", leaf);
+ var root = new AggregateException("root", inner);
+ root.GetSelfAndDescendants().Should().Equal(root, inner, leaf);
+ }
}
}
diff --git a/PowerKit.Tests/FunctionalExtensionsTests.cs b/PowerKit.Tests/FunctionalExtensionsTests.cs
index 76f8e34..449b6b5 100644
--- a/PowerKit.Tests/FunctionalExtensionsTests.cs
+++ b/PowerKit.Tests/FunctionalExtensionsTests.cs
@@ -1,3 +1,4 @@
+using System;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
@@ -9,78 +10,26 @@ public class FunctionalExtensionsTests
[Fact]
public void Pipe_Test()
{
- // Act
- var result = 5.Pipe(x => x * 2);
-
- // Assert
- result.Should().Be(10);
- }
-
- [Fact]
- public void Pipe_Chained_Test()
- {
- // Act
- var result = "hello".Pipe(s => s.ToUpper()).Pipe(s => s + "!");
-
- // Assert
- result.Should().Be("HELLO!");
- }
-
- [Fact]
- public void NullIf_PredicateMatches_Test()
- {
- // Act
- var result = 0.NullIf(v => v == 0);
-
- // Assert
- result.Should().BeNull();
- }
-
- [Fact]
- public void NullIf_PredicateDoesNotMatch_Test()
- {
- // Act
- var result = 5.NullIf(v => v == 0);
-
- // Assert
- result.Should().Be(5);
- }
-
- [Fact]
- public void NullIfDefault_Default_Test()
- {
- // Act
- var result = 0.NullIfDefault();
-
- // Assert
- result.Should().BeNull();
- }
-
- [Fact]
- public void NullIfDefault_NonDefault_Test()
- {
- // Act
- var result = 42.NullIfDefault();
-
- // Assert
- result.Should().Be(42);
+ // Act & assert
+ 5.Pipe(x => x * 2).Should().Be(10);
+ "hello".Pipe(s => s.ToUpper()).Pipe(s => s + "!").Should().Be("HELLO!");
}
[Fact]
- public void NullIfDefault_DefaultGuid_Test()
+ public void NullIf_Test()
{
// Act & assert
- Guid.Empty.NullIfDefault().Should().BeNull();
+ 0.NullIf(v => v == 0).Should().BeNull();
+ 5.NullIf(v => v == 0).Should().Be(5);
}
[Fact]
- public void NullIfDefault_NonDefaultGuid_Test()
+ public void NullIfDefault_Test()
{
- // Arrange
- var id = Guid.NewGuid();
-
// Act & assert
- id.NullIfDefault().Should().Be(id);
+ 0.NullIfDefault().Should().BeNull();
+ 42.NullIfDefault().Should().Be(42);
+ Guid.Empty.NullIfDefault().Should().BeNull();
}
[Fact]
@@ -88,19 +37,7 @@ public void NullIfEmpty_Test()
{
// Act & assert
"hello".NullIfEmpty().Should().Be("hello");
- }
-
- [Fact]
- public void NullIfEmpty_Empty_Test()
- {
- // Act & assert
"".NullIfEmpty().Should().BeNull();
- }
-
- [Fact]
- public void NullIfEmpty_Whitespace_Test()
- {
- // Act & assert
" ".NullIfEmpty().Should().Be(" ");
}
@@ -109,19 +46,7 @@ public void NullIfWhiteSpace_Test()
{
// Act & assert
"hello".NullIfWhiteSpace().Should().Be("hello");
- }
-
- [Fact]
- public void NullIfWhiteSpace_Whitespace_Test()
- {
- // Act & assert
" ".NullIfWhiteSpace().Should().BeNull();
- }
-
- [Fact]
- public void NullIfWhiteSpace_Empty_Test()
- {
- // Act & assert
"".NullIfWhiteSpace().Should().BeNull();
}
}
diff --git a/PowerKit.Tests/ObjectExtensionsTests.cs b/PowerKit.Tests/ObjectExtensionsTests.cs
index 68af34d..f184a3d 100644
--- a/PowerKit.Tests/ObjectExtensionsTests.cs
+++ b/PowerKit.Tests/ObjectExtensionsTests.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
@@ -9,33 +10,9 @@ public class ObjectExtensionsTests
[Fact]
public void ToSingletonEnumerable_Test()
{
- // Act
- var result = 42.ToSingletonEnumerable().ToList();
-
- // Assert
- result.Should().Equal(42);
- }
-
- [Fact]
- public void ToSingletonEnumerable_ReferenceType_Test()
- {
- // Act
- var result = "hello".ToSingletonEnumerable().ToList();
-
- // Assert
- result.Should().Equal("hello");
- }
-
- [Fact]
- public void ToSingletonEnumerable_Null_Test()
- {
- // Arrange
- string? obj = null;
-
- // Act
- var result = obj.ToSingletonEnumerable().ToList();
-
- // Assert
- result.Should().Equal((string?)null);
+ // Act & assert
+ 42.ToSingletonEnumerable().ToList().Should().Equal(42);
+ "hello".ToSingletonEnumerable().ToList().Should().Equal("hello");
+ ((string?)null).ToSingletonEnumerable().ToList().Should().Equal((string?)null);
}
}
diff --git a/PowerKit.Tests/PathExtensionsTests.cs b/PowerKit.Tests/PathExtensionsTests.cs
index b34a8ef..04fd106 100644
--- a/PowerKit.Tests/PathExtensionsTests.cs
+++ b/PowerKit.Tests/PathExtensionsTests.cs
@@ -12,75 +12,15 @@ public void EscapeFileName_Test()
{
// Act & assert
Path.EscapeFileName("hello world.txt").Should().Be("hello world.txt");
- }
-
- [Fact]
- public void EscapeFileName_ForwardSlash_Test()
- {
- // Act & assert
Path.EscapeFileName("a/b").Should().Be("a_b");
- }
-
- [Fact]
- public void EscapeFileName_Backslash_Test()
- {
- // Act & assert
Path.EscapeFileName("a\\b").Should().Be("a_b");
- }
-
- [Fact]
- public void EscapeFileName_Colon_Test()
- {
- // Act & assert
Path.EscapeFileName("C:drive").Should().Be("C_drive");
- }
-
- [Fact]
- public void EscapeFileName_AllInvalidChars_Test()
- {
- // Act & assert
Path.EscapeFileName("a\0b/c\\d:e*f?g\"h
- net9.0
- enable
+ net10.0
enable
false
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index a4966cf..34f2cb3 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -1,4 +1,7 @@
+using System;
+using System.Collections.Generic;
using System.IO;
+using System.Threading.Tasks;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
diff --git a/PowerKit.Tests/StringBuilderExtensionsTests.cs b/PowerKit.Tests/StringBuilderExtensionsTests.cs
index adc81d8..a75cf84 100644
--- a/PowerKit.Tests/StringBuilderExtensionsTests.cs
+++ b/PowerKit.Tests/StringBuilderExtensionsTests.cs
@@ -7,133 +7,25 @@ namespace PowerKit.Tests;
public class StringBuilderExtensionsTests
{
- [Fact]
- public void AppendIfNotEmpty_Empty_Test()
- {
- // Arrange
- var builder = new StringBuilder();
-
- // Act
- builder.AppendIfNotEmpty(',');
-
- // Assert
- builder.ToString().Should().Be("");
- }
-
[Fact]
public void AppendIfNotEmpty_Test()
{
- // Arrange
- var builder = new StringBuilder("hello");
-
- // Act
- builder.AppendIfNotEmpty(',');
-
- // Assert
- builder.ToString().Should().Be("hello,");
- }
-
- [Fact]
- public void AppendIfNotEmpty_Chaining_Test()
- {
- // Arrange
- var builder = new StringBuilder("a");
-
- // Act
- builder.AppendIfNotEmpty(',').Append("b");
-
- // Assert
- builder.ToString().Should().Be("a,b");
+ // Act & assert
+ new StringBuilder().AppendIfNotEmpty(',').ToString().Should().Be("");
+ new StringBuilder("hello").AppendIfNotEmpty(',').ToString().Should().Be("hello,");
+ new StringBuilder("a").AppendIfNotEmpty(',').Append("b").ToString().Should().Be("a,b");
}
[Fact]
public void Trim_Test()
{
- // Arrange
- var builder = new StringBuilder(" hello ");
-
- // Act
- builder.Trim();
-
- // Assert
- builder.ToString().Should().Be("hello");
- }
-
- [Fact]
- public void Trim_LeadingOnly_Test()
- {
- // Arrange
- var builder = new StringBuilder(" hello");
-
- // Act
- builder.Trim();
-
- // Assert
- builder.ToString().Should().Be("hello");
- }
-
- [Fact]
- public void Trim_TrailingOnly_Test()
- {
- // Arrange
- var builder = new StringBuilder("hello ");
-
- // Act
- builder.Trim();
-
- // Assert
- builder.ToString().Should().Be("hello");
- }
-
- [Fact]
- public void Trim_NoWhitespace_Test()
- {
- // Arrange
- var builder = new StringBuilder("hello");
-
- // Act
- builder.Trim();
-
- // Assert
- builder.ToString().Should().Be("hello");
- }
-
- [Fact]
- public void Trim_Empty_Test()
- {
- // Arrange
- var builder = new StringBuilder();
-
- // Act
- builder.Trim();
-
- // Assert
- builder.ToString().Should().Be("");
- }
-
- [Fact]
- public void Trim_OnlyWhitespace_Test()
- {
- // Arrange
- var builder = new StringBuilder(" ");
-
- // Act
- builder.Trim();
-
- // Assert
- builder.ToString().Should().Be("");
- }
-
- [Fact]
- public void Trim_Chaining_Test()
- {
- // Arrange
- var builder = new StringBuilder(" hello ");
-
- // Act
- var result = builder.Trim().Append("!");
-
- // Assert
- result.ToString().Should().Be("hello!");
+ // Act & assert
+ new StringBuilder(" hello ").Trim().ToString().Should().Be("hello");
+ new StringBuilder(" hello").Trim().ToString().Should().Be("hello");
+ new StringBuilder("hello ").Trim().ToString().Should().Be("hello");
+ new StringBuilder("hello").Trim().ToString().Should().Be("hello");
+ new StringBuilder().Trim().ToString().Should().Be("");
+ new StringBuilder(" ").Trim().ToString().Should().Be("");
+ new StringBuilder(" hello ").Trim().Append("!").ToString().Should().Be("hello!");
}
}
diff --git a/PowerKit.Tests/StringExtensionsTests.cs b/PowerKit.Tests/StringExtensionsTests.cs
index eed8fca..3f23070 100644
--- a/PowerKit.Tests/StringExtensionsTests.cs
+++ b/PowerKit.Tests/StringExtensionsTests.cs
@@ -11,19 +11,7 @@ public void SubstringUntil_Test()
{
// Act & assert
"hello world".SubstringUntil(" ").Should().Be("hello");
- }
-
- [Fact]
- public void SubstringUntil_NotFound_Test()
- {
- // Act & assert
"hello".SubstringUntil("x").Should().Be("hello");
- }
-
- [Fact]
- public void SubstringUntil_AtStart_Test()
- {
- // Act & assert
"xhello".SubstringUntil("x").Should().Be("");
}
@@ -32,40 +20,16 @@ public void SubstringAfter_Test()
{
// Act & assert
"hello world".SubstringAfter(" ").Should().Be("world");
- }
-
- [Fact]
- public void SubstringAfter_NotFound_Test()
- {
- // Act & assert
"hello".SubstringAfter("x").Should().Be("");
- }
-
- [Fact]
- public void SubstringAfter_AtEnd_Test()
- {
- // Act & assert
"hellox".SubstringAfter("x").Should().Be("");
}
[Fact]
- public void Truncate_ShorterThanLimit_Test()
+ public void Truncate_Test()
{
// Act & assert
"hi".Truncate(10).Should().Be("hi");
- }
-
- [Fact]
- public void Truncate_ExactlyAtLimit_Test()
- {
- // Act & assert
"hello".Truncate(5).Should().Be("hello");
- }
-
- [Fact]
- public void Truncate_LongerThanLimit_Test()
- {
- // Act & assert
"hello".Truncate(3).Should().Be("hel");
}
@@ -74,40 +38,10 @@ public void SeparateWords_Test()
{
// Act & assert
"HelloWorld".SeparateWords(' ').Should().Be("Hello World");
- }
-
- [Fact]
- public void SeparateWords_SingleWord_Test()
- {
- // Act & assert
"Hello".SeparateWords(' ').Should().Be("Hello");
- }
-
- [Fact]
- public void SeparateWords_AllLowercase_Test()
- {
- // Act & assert
"hello".SeparateWords(' ').Should().Be("hello");
- }
-
- [Fact]
- public void SeparateWords_Empty_Test()
- {
- // Act & assert
"".SeparateWords(' ').Should().Be("");
- }
-
- [Fact]
- public void SeparateWords_Multiple_Test()
- {
- // Act & assert
"FooBarBaz".SeparateWords(' ').Should().Be("Foo Bar Baz");
- }
-
- [Fact]
- public void SeparateWords_CustomSeparator_Test()
- {
- // Act & assert
"FooBarBaz".SeparateWords('-').Should().Be("Foo-Bar-Baz");
}
}
diff --git a/PowerKit.Tests/TextReaderExtensionsTests.cs b/PowerKit.Tests/TextReaderExtensionsTests.cs
index 2a6e65d..3043e85 100644
--- a/PowerKit.Tests/TextReaderExtensionsTests.cs
+++ b/PowerKit.Tests/TextReaderExtensionsTests.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Threading.Tasks;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
@@ -10,39 +11,17 @@ public class TextReaderExtensionsTests
[Fact]
public async Task ReadLinesAsync_Test()
{
- // Arrange
- using var reader = new StringReader("line1\nline2\nline3");
-
- // Act
- var lines = await reader.ReadLinesAsync().ToListAsync();
-
- // Assert
- lines.Should().Equal("line1", "line2", "line3");
- }
-
- [Fact]
- public async Task ReadLinesAsync_Empty_Test()
- {
- // Arrange
- using var reader = new StringReader("");
-
- // Act
- var lines = await reader.ReadLinesAsync().ToListAsync();
-
- // Assert
- lines.Should().BeEmpty();
- }
-
- [Fact]
- public async Task ReadLinesAsync_SingleLine_Test()
- {
- // Arrange
- using var reader = new StringReader("hello");
-
- // Act
- var lines = await reader.ReadLinesAsync().ToListAsync();
-
- // Assert
- lines.Should().Equal("hello");
+ // Act & assert
+ (await new StringReader("line1\nline2\nline3").ReadLinesAsync().ToListAsync())
+ .Should()
+ .Equal("line1", "line2", "line3");
+
+ (await new StringReader("").ReadLinesAsync().ToListAsync())
+ .Should()
+ .BeEmpty();
+
+ (await new StringReader("hello").ReadLinesAsync().ToListAsync())
+ .Should()
+ .Equal("hello");
}
}
From 65b6ff313f347ff50a785e3e9b00cfad5cb2060c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 20:07:30 +0000
Subject: [PATCH 24/43] Add non-default Guid test case to NullIfDefault_Test
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/9d5de743-75ab-4f65-8a3e-3bbf5b6e0a4a
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/FunctionalExtensionsTests.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/PowerKit.Tests/FunctionalExtensionsTests.cs b/PowerKit.Tests/FunctionalExtensionsTests.cs
index 449b6b5..ee9c76c 100644
--- a/PowerKit.Tests/FunctionalExtensionsTests.cs
+++ b/PowerKit.Tests/FunctionalExtensionsTests.cs
@@ -30,6 +30,7 @@ public void NullIfDefault_Test()
0.NullIfDefault().Should().BeNull();
42.NullIfDefault().Should().Be(42);
Guid.Empty.NullIfDefault().Should().BeNull();
+ Guid.NewGuid().NullIfDefault().Should().NotBeNull();
}
[Fact]
From 502c93a51a924ab135f5504d4bfff96d53718d64 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 20:11:14 +0000
Subject: [PATCH 25/43] Add ToKebabCase() and ToSnakeCase() string extensions
with tests
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/a502dd1b-aac8-4517-b6f4-f930bd9b8752
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/StringExtensionsTests.cs | 22 ++++++++++++++++++++++
PowerKit/Extensions/StringExtensions.cs | 4 ++++
2 files changed, 26 insertions(+)
diff --git a/PowerKit.Tests/StringExtensionsTests.cs b/PowerKit.Tests/StringExtensionsTests.cs
index 3f23070..8b0b4a0 100644
--- a/PowerKit.Tests/StringExtensionsTests.cs
+++ b/PowerKit.Tests/StringExtensionsTests.cs
@@ -44,4 +44,26 @@ public void SeparateWords_Test()
"FooBarBaz".SeparateWords(' ').Should().Be("Foo Bar Baz");
"FooBarBaz".SeparateWords('-').Should().Be("Foo-Bar-Baz");
}
+
+ [Fact]
+ public void ToKebabCase_Test()
+ {
+ // Act & assert
+ "HelloWorld".ToKebabCase().Should().Be("hello-world");
+ "FooBarBaz".ToKebabCase().Should().Be("foo-bar-baz");
+ "Hello".ToKebabCase().Should().Be("hello");
+ "hello".ToKebabCase().Should().Be("hello");
+ "".ToKebabCase().Should().Be("");
+ }
+
+ [Fact]
+ public void ToSnakeCase_Test()
+ {
+ // Act & assert
+ "HelloWorld".ToSnakeCase().Should().Be("hello_world");
+ "FooBarBaz".ToSnakeCase().Should().Be("foo_bar_baz");
+ "Hello".ToSnakeCase().Should().Be("hello");
+ "hello".ToSnakeCase().Should().Be("hello");
+ "".ToSnakeCase().Should().Be("");
+ }
}
diff --git a/PowerKit/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs
index c002f63..c6ad9d8 100644
--- a/PowerKit/Extensions/StringExtensions.cs
+++ b/PowerKit/Extensions/StringExtensions.cs
@@ -45,5 +45,9 @@ public string SeparateWords(char separator)
return builder.ToString();
}
+
+ public string ToKebabCase() => str.SeparateWords('-').ToLowerInvariant();
+
+ public string ToSnakeCase() => str.SeparateWords('_').ToLowerInvariant();
}
}
From ce8971607a62cb4f70de2952e1eddc2ff97985d8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Apr 2026 20:20:29 +0000
Subject: [PATCH 26/43] Address review feedback: unmerge tests, fix
Disposable.Merge exception handling, move ToSingletonEnumerable, clean up
StreamExtensions
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/e596216f-ce6b-4c78-bd26-1e6796eea22d
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
.../AggregateExceptionExtensionsTests.cs | 39 ++++++---
PowerKit.Tests/DisposableTests.cs | 33 ++++++--
PowerKit.Tests/EnumerableExtensionsTests.cs | 11 +++
PowerKit.Tests/ExceptionExtensionsTests.cs | 84 ++++++++++++-------
PowerKit.Tests/ObjectExtensionsTests.cs | 18 ----
PowerKit.Tests/StreamExtensionsTests.cs | 59 -------------
PowerKit.Tests/TextReaderExtensionsTests.cs | 46 +++++++---
PowerKit/Disposable.cs | 18 +++-
PowerKit/Extensions/EnumerableExtensions.cs | 8 ++
PowerKit/Extensions/ObjectExtensions.cs | 14 ----
PowerKit/Extensions/StreamExtensions.cs | 4 +-
11 files changed, 177 insertions(+), 157 deletions(-)
delete mode 100644 PowerKit.Tests/ObjectExtensionsTests.cs
delete mode 100644 PowerKit/Extensions/ObjectExtensions.cs
diff --git a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
index 9ad8752..64a317b 100644
--- a/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
+++ b/PowerKit.Tests/AggregateExceptionExtensionsTests.cs
@@ -10,25 +10,40 @@ public class AggregateExceptionExtensionsTests
[Fact]
public void TryGetSingle_Test()
{
- {
- // Act & assert
- var inner = new Exception("only");
- new AggregateException(inner).TryGetSingle().Should().BeSameAs(inner);
- }
+ // Arrange
+ var inner = new Exception("only");
+ // Act & assert
+ new AggregateException(inner).TryGetSingle().Should().BeSameAs(inner);
+ }
+
+ [Fact]
+ public void TryGetSingle_Multiple_Test()
+ {
+ // Act & assert
new AggregateException(new Exception("a"), new Exception("b"))
.TryGetSingle()
.Should()
.BeNull();
+ }
- {
- var leaf = new Exception("leaf");
- new AggregateException(new AggregateException(leaf))
- .TryGetSingle()
- .Should()
- .BeSameAs(leaf);
- }
+ [Fact]
+ public void TryGetSingle_Nested_Test()
+ {
+ // Arrange
+ var leaf = new Exception("leaf");
+ // Act & assert
+ new AggregateException(new AggregateException(leaf))
+ .TryGetSingle()
+ .Should()
+ .BeSameAs(leaf);
+ }
+
+ [Fact]
+ public void TryGetSingle_NestedMultiple_Test()
+ {
+ // Act & assert
new AggregateException(
new AggregateException(new Exception("a"), new Exception("b"))
)
diff --git a/PowerKit.Tests/DisposableTests.cs b/PowerKit.Tests/DisposableTests.cs
index d76edcd..3ac1edd 100644
--- a/PowerKit.Tests/DisposableTests.cs
+++ b/PowerKit.Tests/DisposableTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
@@ -8,13 +9,6 @@ namespace PowerKit.Tests;
public class DisposableTests
{
- [Fact]
- public void Null_Test()
- {
- // Act & assert
- Disposable.Null.Dispose();
- }
-
[Fact]
public void Create_Test()
{
@@ -44,4 +38,29 @@ public void Merge_Test()
// Assert
order.Should().Equal(0, 1, 2);
}
+
+ [Fact]
+ public void Merge_Exception_Test()
+ {
+ // Arrange
+ var disposed = new List();
+ var disposables = new[]
+ {
+ Disposable.Create(() => disposed.Add(0)),
+ Disposable.Create(() =>
+ {
+ disposed.Add(1);
+ throw new InvalidOperationException("fail");
+ }),
+ Disposable.Create(() => disposed.Add(2)),
+ };
+
+ // Act
+ var ex = Assert.Throws(() => Disposable.Merge(disposables).Dispose());
+
+ // Assert
+ disposed.Should().Equal(0, 1, 2);
+ ex.InnerExceptions.Should().ContainSingle();
+ ex.InnerExceptions[0].Should().BeOfType();
+ }
}
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
index 9330576..d0a601e 100644
--- a/PowerKit.Tests/EnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
@@ -7,6 +8,15 @@ namespace PowerKit.Tests;
public class EnumerableExtensionsTests
{
+ [Fact]
+ public void ToSingletonEnumerable_Test()
+ {
+ // Act & assert
+ 42.ToSingletonEnumerable().ToList().Should().Equal(42);
+ "hello".ToSingletonEnumerable().ToList().Should().Equal("hello");
+ ((string?)null).ToSingletonEnumerable().ToList().Should().Equal((string?)null);
+ }
+
[Fact]
public void WhereNotNull_Test()
{
@@ -31,6 +41,7 @@ public void FirstOrNull_Test()
{
// Act & assert
new[] { 5, 10, 15 }.FirstOrNull().Should().Be(5);
+ new[] { 42 }.FirstOrNull().Should().Be(42);
Array.Empty().FirstOrNull().Should().BeNull();
}
diff --git a/PowerKit.Tests/ExceptionExtensionsTests.cs b/PowerKit.Tests/ExceptionExtensionsTests.cs
index 01d70a0..c3a46ad 100644
--- a/PowerKit.Tests/ExceptionExtensionsTests.cs
+++ b/PowerKit.Tests/ExceptionExtensionsTests.cs
@@ -10,37 +10,57 @@ public class ExceptionExtensionsTests
[Fact]
public void GetSelfAndDescendants_Test()
{
- {
- // Act & assert
- var ex = new Exception("root");
- ex.GetSelfAndDescendants().Should().Equal(ex);
- }
-
- {
- var inner = new Exception("inner");
- var outer = new Exception("outer", inner);
- outer.GetSelfAndDescendants().Should().Equal(outer, inner);
- }
-
- {
- var leaf = new Exception("leaf");
- var middle = new Exception("middle", leaf);
- var root = new Exception("root", middle);
- root.GetSelfAndDescendants().Should().Equal(root, middle, leaf);
- }
-
- {
- var inner1 = new Exception("inner1");
- var inner2 = new Exception("inner2");
- var aggregate = new AggregateException("aggregate", inner1, inner2);
- aggregate.GetSelfAndDescendants().Should().Equal(aggregate, inner1, inner2);
- }
-
- {
- var leaf = new Exception("leaf");
- var inner = new AggregateException("inner", leaf);
- var root = new AggregateException("root", inner);
- root.GetSelfAndDescendants().Should().Equal(root, inner, leaf);
- }
+ // Arrange
+ var ex = new Exception("root");
+
+ // Act & assert
+ ex.GetSelfAndDescendants().Should().Equal(ex);
+ }
+
+ [Fact]
+ public void GetSelfAndDescendants_Chain_Test()
+ {
+ // Arrange
+ var inner = new Exception("inner");
+ var outer = new Exception("outer", inner);
+
+ // Act & assert
+ outer.GetSelfAndDescendants().Should().Equal(outer, inner);
+ }
+
+ [Fact]
+ public void GetSelfAndDescendants_DeepChain_Test()
+ {
+ // Arrange
+ var leaf = new Exception("leaf");
+ var middle = new Exception("middle", leaf);
+ var root = new Exception("root", middle);
+
+ // Act & assert
+ root.GetSelfAndDescendants().Should().Equal(root, middle, leaf);
+ }
+
+ [Fact]
+ public void GetSelfAndDescendants_Aggregate_Test()
+ {
+ // Arrange
+ var inner1 = new Exception("inner1");
+ var inner2 = new Exception("inner2");
+ var aggregate = new AggregateException("aggregate", inner1, inner2);
+
+ // Act & assert
+ aggregate.GetSelfAndDescendants().Should().Equal(aggregate, inner1, inner2);
+ }
+
+ [Fact]
+ public void GetSelfAndDescendants_NestedAggregate_Test()
+ {
+ // Arrange
+ var leaf = new Exception("leaf");
+ var inner = new AggregateException("inner", leaf);
+ var root = new AggregateException("root", inner);
+
+ // Act & assert
+ root.GetSelfAndDescendants().Should().Equal(root, inner, leaf);
}
}
diff --git a/PowerKit.Tests/ObjectExtensionsTests.cs b/PowerKit.Tests/ObjectExtensionsTests.cs
deleted file mode 100644
index f184a3d..0000000
--- a/PowerKit.Tests/ObjectExtensionsTests.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Linq;
-using FluentAssertions;
-using PowerKit.Extensions;
-using Xunit;
-
-namespace PowerKit.Tests;
-
-public class ObjectExtensionsTests
-{
- [Fact]
- public void ToSingletonEnumerable_Test()
- {
- // Act & assert
- 42.ToSingletonEnumerable().ToList().Should().Equal(42);
- "hello".ToSingletonEnumerable().ToList().Should().Equal("hello");
- ((string?)null).ToSingletonEnumerable().ToList().Should().Equal((string?)null);
- }
-}
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index 34f2cb3..7fa9587 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -25,35 +25,6 @@ public async Task CopyToAsync_AutoFlush_Test()
destination.ToArray().Should().Equal(data);
}
- [Fact]
- public async Task CopyToAsync_NoAutoFlush_Test()
- {
- // Arrange
- var data = new byte[] { 10, 20, 30 };
- using var source = new MemoryStream(data);
- using var destination = new MemoryStream();
-
- // Act
- await source.CopyToAsync(destination, autoFlush: false);
-
- // Assert
- destination.ToArray().Should().Equal(data);
- }
-
- [Fact]
- public async Task CopyToAsync_Empty_Test()
- {
- // Arrange
- using var source = new MemoryStream();
- using var destination = new MemoryStream();
-
- // Act
- await source.CopyToAsync(destination, autoFlush: false);
-
- // Assert
- destination.ToArray().Should().BeEmpty();
- }
-
[Fact]
public async Task CopyToAsync_Progress_Test()
{
@@ -62,21 +33,6 @@ public async Task CopyToAsync_Progress_Test()
using var source = new MemoryStream(data);
using var destination = new MemoryStream();
- // Act
- await source.CopyToAsync(destination, progress: null);
-
- // Assert
- destination.ToArray().Should().Equal(data);
- }
-
- [Fact]
- public async Task CopyToAsync_Progress_Reports_Test()
- {
- // Arrange
- var data = new byte[1024];
- using var source = new MemoryStream(data);
- using var destination = new MemoryStream();
-
var reports = new List();
var progress = new Progress(v => reports.Add(v));
@@ -92,21 +48,6 @@ public async Task CopyToAsync_Progress_Reports_Test()
reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
}
- [Fact]
- public async Task CopyToAsync_ContentLength_Test()
- {
- // Arrange
- var data = new byte[1024];
- using var source = new MemoryStream(data);
- using var destination = new MemoryStream();
-
- // Act
- await source.CopyToAsync(destination, contentLength: 1024);
-
- // Assert
- destination.ToArray().Should().Equal(data);
- }
-
[Fact]
public async Task CopyToAsync_ContentLength_Progress_Test()
{
diff --git a/PowerKit.Tests/TextReaderExtensionsTests.cs b/PowerKit.Tests/TextReaderExtensionsTests.cs
index 3043e85..2ec67a3 100644
--- a/PowerKit.Tests/TextReaderExtensionsTests.cs
+++ b/PowerKit.Tests/TextReaderExtensionsTests.cs
@@ -11,17 +11,39 @@ public class TextReaderExtensionsTests
[Fact]
public async Task ReadLinesAsync_Test()
{
- // Act & assert
- (await new StringReader("line1\nline2\nline3").ReadLinesAsync().ToListAsync())
- .Should()
- .Equal("line1", "line2", "line3");
-
- (await new StringReader("").ReadLinesAsync().ToListAsync())
- .Should()
- .BeEmpty();
-
- (await new StringReader("hello").ReadLinesAsync().ToListAsync())
- .Should()
- .Equal("hello");
+ // Arrange
+ using var reader = new StringReader("line1\nline2\nline3");
+
+ // Act
+ var result = await reader.ReadLinesAsync().ToListAsync();
+
+ // Assert
+ result.Should().Equal("line1", "line2", "line3");
+ }
+
+ [Fact]
+ public async Task ReadLinesAsync_Empty_Test()
+ {
+ // Arrange
+ using var reader = new StringReader("");
+
+ // Act
+ var result = await reader.ReadLinesAsync().ToListAsync();
+
+ // Assert
+ result.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task ReadLinesAsync_SingleLine_Test()
+ {
+ // Arrange
+ using var reader = new StringReader("hello");
+
+ // Act
+ var result = await reader.ReadLinesAsync().ToListAsync();
+
+ // Assert
+ result.Should().Equal("hello");
}
}
diff --git a/PowerKit/Disposable.cs b/PowerKit/Disposable.cs
index c79eed0..78c6987 100644
--- a/PowerKit/Disposable.cs
+++ b/PowerKit/Disposable.cs
@@ -17,7 +17,23 @@ internal partial class Disposable
public static IDisposable Merge(params IEnumerable disposables) =>
Create(() =>
{
+ var exceptions = new List();
+
foreach (var disposable in disposables)
- disposable.Dispose();
+ {
+ try
+ {
+ disposable.Dispose();
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ }
+ }
+
+ if (exceptions.Count > 0)
+ {
+ throw new AggregateException(exceptions);
+ }
});
}
diff --git a/PowerKit/Extensions/EnumerableExtensions.cs b/PowerKit/Extensions/EnumerableExtensions.cs
index 8bdc534..6deec43 100644
--- a/PowerKit/Extensions/EnumerableExtensions.cs
+++ b/PowerKit/Extensions/EnumerableExtensions.cs
@@ -5,6 +5,14 @@ namespace PowerKit.Extensions;
internal static class EnumerableExtensions
{
+ extension(T obj)
+ {
+ public IEnumerable ToSingletonEnumerable()
+ {
+ yield return obj;
+ }
+ }
+
extension(IEnumerable source)
where T : class
{
diff --git a/PowerKit/Extensions/ObjectExtensions.cs b/PowerKit/Extensions/ObjectExtensions.cs
deleted file mode 100644
index 4225646..0000000
--- a/PowerKit/Extensions/ObjectExtensions.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-
-namespace PowerKit.Extensions;
-
-internal static class ObjectExtensions
-{
- extension(T obj)
- {
- public IEnumerable ToSingletonEnumerable()
- {
- yield return obj;
- }
- }
-}
diff --git a/PowerKit/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs
index fb4c523..38ff1cf 100644
--- a/PowerKit/Extensions/StreamExtensions.cs
+++ b/PowerKit/Extensions/StreamExtensions.cs
@@ -73,7 +73,7 @@ await destination
public async ValueTask CopyToAsync(
Stream destination,
long contentLength,
- IProgress? progress = null,
+ IProgress progress,
CancellationToken cancellationToken = default
)
{
@@ -98,7 +98,7 @@ await destination
totalBytesRead += bytesRead;
- if (progress is not null && contentLength > 0)
+ if (contentLength > 0)
{
progress.Report(1.0 * totalBytesRead / contentLength);
}
From f5626864ed256bee00843ea11a39601046485423 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:11:48 +0000
Subject: [PATCH 27/43] Use ToArray() instead of ToList() in
ToSingletonEnumerable test
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/ba7fd89b-de46-401f-ae6c-f82a3b566f6c
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/EnumerableExtensionsTests.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
index d0a601e..8f4b326 100644
--- a/PowerKit.Tests/EnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -12,9 +12,9 @@ public class EnumerableExtensionsTests
public void ToSingletonEnumerable_Test()
{
// Act & assert
- 42.ToSingletonEnumerable().ToList().Should().Equal(42);
- "hello".ToSingletonEnumerable().ToList().Should().Equal("hello");
- ((string?)null).ToSingletonEnumerable().ToList().Should().Equal((string?)null);
+ 42.ToSingletonEnumerable().ToArray().Should().Equal(42);
+ "hello".ToSingletonEnumerable().ToArray().Should().Equal("hello");
+ ((string?)null).ToSingletonEnumerable().ToArray().Should().Equal((string?)null);
}
[Fact]
From 47da80b57e630c98cc111f3745260770bf084e40 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:15:19 +0000
Subject: [PATCH 28/43] Replace Progress with synchronous IProgress in
stream tests
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/de9060b6-d912-4603-bbcc-379e6fe1753c
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/StreamExtensionsTests.cs | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index 7fa9587..99308de 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -34,14 +34,11 @@ public async Task CopyToAsync_Progress_Test()
using var destination = new MemoryStream();
var reports = new List();
- var progress = new Progress(v => reports.Add(v));
+ var progress = new SynchronousProgress(v => reports.Add(v));
// Act
await source.CopyToAsync(destination, progress: progress);
- // Allow Progress callbacks to fire on the thread pool
- await Task.Delay(50);
-
// Assert
reports.Should().NotBeEmpty();
reports.Should().AllSatisfy(v => v.Should().BeInRange(0.0, 1.0));
@@ -57,17 +54,19 @@ public async Task CopyToAsync_ContentLength_Progress_Test()
using var destination = new MemoryStream();
var reports = new List();
- var progress = new Progress(v => reports.Add(v));
+ var progress = new SynchronousProgress(v => reports.Add(v));
// Act
await source.CopyToAsync(destination, contentLength: 1024, progress: progress);
- // Allow Progress callbacks to fire on the thread pool
- await Task.Delay(50);
-
// Assert
reports.Should().NotBeEmpty();
reports.Should().AllSatisfy(v => v.Should().BeInRange(0.0, 1.0));
reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
}
}
+
+file class SynchronousProgress(Action handler) : IProgress
+{
+ public void Report(T value) => handler(value);
+}
From ae8a23c6be7a25440e8211a210b6d6f4ad494a7e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:21:27 +0000
Subject: [PATCH 29/43] Add WhereNotNullOrEmpty, refactor StreamExtensions to
share code, lazy-init exceptions in Disposable.Merge
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/7e8c07e4-d0e1-4317-94db-021dd5d0ed06
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/EnumerableExtensionsTests.cs | 14 ++++++++
PowerKit/Disposable.cs | 6 ++--
PowerKit/Extensions/EnumerableExtensions.cs | 17 ++++++++++
PowerKit/Extensions/StreamExtensions.cs | 36 +++++----------------
4 files changed, 42 insertions(+), 31 deletions(-)
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
index 8f4b326..f0f477b 100644
--- a/PowerKit.Tests/EnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -26,6 +26,20 @@ public void WhereNotNull_Test()
Array.Empty().WhereNotNull().Should().BeEmpty();
}
+ [Fact]
+ public void WhereNotNullOrEmpty_Test()
+ {
+ // Act & assert
+ new string?[] { "hello", null, "", "world" }
+ .WhereNotNullOrEmpty()
+ .Should()
+ .Equal("hello", "world");
+ new string?[] { "hello", " ", "", null }
+ .WhereNotNullOrEmpty()
+ .Should()
+ .Equal("hello", " ");
+ }
+
[Fact]
public void WhereNotNullOrWhiteSpace_Test()
{
diff --git a/PowerKit/Disposable.cs b/PowerKit/Disposable.cs
index 78c6987..eb2179f 100644
--- a/PowerKit/Disposable.cs
+++ b/PowerKit/Disposable.cs
@@ -17,7 +17,7 @@ internal partial class Disposable
public static IDisposable Merge(params IEnumerable disposables) =>
Create(() =>
{
- var exceptions = new List();
+ List? exceptions = null;
foreach (var disposable in disposables)
{
@@ -27,11 +27,11 @@ public static IDisposable Merge(params IEnumerable disposables) =>
}
catch (Exception ex)
{
- exceptions.Add(ex);
+ (exceptions ??= []).Add(ex);
}
}
- if (exceptions.Count > 0)
+ if (exceptions?.Count > 0)
{
throw new AggregateException(exceptions);
}
diff --git a/PowerKit/Extensions/EnumerableExtensions.cs b/PowerKit/Extensions/EnumerableExtensions.cs
index 6deec43..378c712 100644
--- a/PowerKit/Extensions/EnumerableExtensions.cs
+++ b/PowerKit/Extensions/EnumerableExtensions.cs
@@ -21,7 +21,9 @@ public IEnumerable WhereNotNull()
foreach (var item in source)
{
if (item is not null)
+ {
yield return item;
+ }
}
}
}
@@ -34,19 +36,34 @@ public IEnumerable WhereNotNull()
foreach (var item in source)
{
if (item is not null)
+ {
yield return item.Value;
+ }
}
}
}
extension(IEnumerable source)
{
+ public IEnumerable WhereNotNullOrEmpty()
+ {
+ foreach (var item in source)
+ {
+ if (!string.IsNullOrEmpty(item))
+ {
+ yield return item!;
+ }
+ }
+ }
+
public IEnumerable WhereNotNullOrWhiteSpace()
{
foreach (var item in source)
{
if (!string.IsNullOrWhiteSpace(item))
+ {
yield return item!;
+ }
}
}
}
diff --git a/PowerKit/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs
index 38ff1cf..343ff57 100644
--- a/PowerKit/Extensions/StreamExtensions.cs
+++ b/PowerKit/Extensions/StreamExtensions.cs
@@ -25,14 +25,18 @@ public async Task CopyToAsync(
.ConfigureAwait(false);
if (bytesRead <= 0)
+ {
break;
+ }
await destination
.WriteAsync(buffer.Memory[..bytesRead], cancellationToken)
.ConfigureAwait(false);
if (autoFlush)
+ {
await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
+ }
}
}
@@ -42,38 +46,14 @@ public async ValueTask CopyToAsync(
CancellationToken cancellationToken = default
)
{
- using var buffer = MemoryPool.Shared.Rent(81920);
-
- var totalBytesRead = 0L;
-
- while (true)
- {
- var bytesRead = await source
- .ReadAsync(buffer.Memory, cancellationToken)
- .ConfigureAwait(false);
-
- if (bytesRead <= 0)
- {
- break;
- }
-
- await destination
- .WriteAsync(buffer.Memory[..bytesRead], cancellationToken)
- .ConfigureAwait(false);
-
- totalBytesRead += bytesRead;
-
- if (progress is not null && source.CanSeek && source.Length > 0)
- {
- progress.Report(1.0 * totalBytesRead / source.Length);
- }
- }
+ var contentLength = source.CanSeek ? source.Length : -1;
+ await source.CopyToAsync(destination, contentLength, progress, cancellationToken);
}
public async ValueTask CopyToAsync(
Stream destination,
long contentLength,
- IProgress progress,
+ IProgress? progress,
CancellationToken cancellationToken = default
)
{
@@ -98,7 +78,7 @@ await destination
totalBytesRead += bytesRead;
- if (contentLength > 0)
+ if (progress is not null && contentLength > 0)
{
progress.Report(1.0 * totalBytesRead / contentLength);
}
From 57870c8bf4f501a92e1808bee1d3fbf827b12b33 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:23:20 +0000
Subject: [PATCH 30/43] Add empty-array test case for WhereNotNullOrEmpty
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/7e8c07e4-d0e1-4317-94db-021dd5d0ed06
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/EnumerableExtensionsTests.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/PowerKit.Tests/EnumerableExtensionsTests.cs b/PowerKit.Tests/EnumerableExtensionsTests.cs
index f0f477b..c32dc15 100644
--- a/PowerKit.Tests/EnumerableExtensionsTests.cs
+++ b/PowerKit.Tests/EnumerableExtensionsTests.cs
@@ -38,6 +38,7 @@ public void WhereNotNullOrEmpty_Test()
.WhereNotNullOrEmpty()
.Should()
.Equal("hello", " ");
+ Array.Empty().WhereNotNullOrEmpty().Should().BeEmpty();
}
[Fact]
From 231c828936491e45177c779c80d753e3ff473b1c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:38:38 +0000
Subject: [PATCH 31/43] Replace custom SynchronousProgress with Gress
package's DelegateProgress
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/ecac02d7-08e2-43dc-b7ea-cf8e9ab88ccb
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
Directory.Packages.props | 1 +
PowerKit.Tests/PowerKit.Tests.csproj | 1 +
PowerKit.Tests/StreamExtensionsTests.cs | 11 +++--------
3 files changed, 5 insertions(+), 8 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0afbb3b..04d12ca 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,6 +6,7 @@
+
diff --git a/PowerKit.Tests/PowerKit.Tests.csproj b/PowerKit.Tests/PowerKit.Tests.csproj
index a3281be..613d7df 100644
--- a/PowerKit.Tests/PowerKit.Tests.csproj
+++ b/PowerKit.Tests/PowerKit.Tests.csproj
@@ -14,6 +14,7 @@
+
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index 99308de..c52b978 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -1,8 +1,8 @@
-using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
+using Gress;
using PowerKit.Extensions;
using Xunit;
@@ -34,7 +34,7 @@ public async Task CopyToAsync_Progress_Test()
using var destination = new MemoryStream();
var reports = new List();
- var progress = new SynchronousProgress(v => reports.Add(v));
+ var progress = new DelegateProgress(v => reports.Add(v));
// Act
await source.CopyToAsync(destination, progress: progress);
@@ -54,7 +54,7 @@ public async Task CopyToAsync_ContentLength_Progress_Test()
using var destination = new MemoryStream();
var reports = new List();
- var progress = new SynchronousProgress(v => reports.Add(v));
+ var progress = new DelegateProgress(v => reports.Add(v));
// Act
await source.CopyToAsync(destination, contentLength: 1024, progress: progress);
@@ -65,8 +65,3 @@ public async Task CopyToAsync_ContentLength_Progress_Test()
reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
}
}
-
-file class SynchronousProgress(Action handler) : IProgress
-{
- public void Report(T value) => handler(value);
-}
From 1027ff47d155e7177e8809733d42e749e691af8e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:45:00 +0000
Subject: [PATCH 32/43] Use Gress ProgressCollector in stream tests; reorder
StreamExtensions methods
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/a4aa3bce-c192-4069-aaec-55a6553835d2
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/StreamExtensionsTests.cs | 10 +++++-----
PowerKit/Extensions/StreamExtensions.cs | 20 ++++++++++----------
2 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index c52b978..d5ef160 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -1,5 +1,5 @@
-using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Gress;
@@ -33,13 +33,13 @@ public async Task CopyToAsync_Progress_Test()
using var source = new MemoryStream(data);
using var destination = new MemoryStream();
- var reports = new List();
- var progress = new DelegateProgress(v => reports.Add(v));
+ var progress = new ProgressCollector();
// Act
await source.CopyToAsync(destination, progress: progress);
// Assert
+ var reports = progress.GetValues().ToArray();
reports.Should().NotBeEmpty();
reports.Should().AllSatisfy(v => v.Should().BeInRange(0.0, 1.0));
reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
@@ -53,13 +53,13 @@ public async Task CopyToAsync_ContentLength_Progress_Test()
using var source = new MemoryStream(data);
using var destination = new MemoryStream();
- var reports = new List();
- var progress = new DelegateProgress(v => reports.Add(v));
+ var progress = new ProgressCollector();
// Act
await source.CopyToAsync(destination, contentLength: 1024, progress: progress);
// Assert
+ var reports = progress.GetValues().ToArray();
reports.Should().NotBeEmpty();
reports.Should().AllSatisfy(v => v.Should().BeInRange(0.0, 1.0));
reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
diff --git a/PowerKit/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs
index 343ff57..de41fd5 100644
--- a/PowerKit/Extensions/StreamExtensions.cs
+++ b/PowerKit/Extensions/StreamExtensions.cs
@@ -40,16 +40,6 @@ await destination
}
}
- public async ValueTask CopyToAsync(
- Stream destination,
- IProgress? progress = null,
- CancellationToken cancellationToken = default
- )
- {
- var contentLength = source.CanSeek ? source.Length : -1;
- await source.CopyToAsync(destination, contentLength, progress, cancellationToken);
- }
-
public async ValueTask CopyToAsync(
Stream destination,
long contentLength,
@@ -84,5 +74,15 @@ await destination
}
}
}
+
+ public async ValueTask CopyToAsync(
+ Stream destination,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var contentLength = source.CanSeek ? source.Length : -1;
+ await source.CopyToAsync(destination, contentLength, progress, cancellationToken);
+ }
}
}
From fd61e3285d34d260a3bae7dbdb2481f2f1eaf597 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Apr 2026 18:52:04 +0000
Subject: [PATCH 33/43] Remove duplicate
CopyToAsync_ContentLength_Progress_Test (same as CopyToAsync_Progress_Test)
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/f8dd53be-d7fe-4818-a11e-69427b83c252
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/StreamExtensionsTests.cs | 19 -------------------
1 file changed, 19 deletions(-)
diff --git a/PowerKit.Tests/StreamExtensionsTests.cs b/PowerKit.Tests/StreamExtensionsTests.cs
index d5ef160..5930c6d 100644
--- a/PowerKit.Tests/StreamExtensionsTests.cs
+++ b/PowerKit.Tests/StreamExtensionsTests.cs
@@ -45,23 +45,4 @@ public async Task CopyToAsync_Progress_Test()
reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
}
- [Fact]
- public async Task CopyToAsync_ContentLength_Progress_Test()
- {
- // Arrange
- var data = new byte[1024];
- using var source = new MemoryStream(data);
- using var destination = new MemoryStream();
-
- var progress = new ProgressCollector();
-
- // Act
- await source.CopyToAsync(destination, contentLength: 1024, progress: progress);
-
- // Assert
- var reports = progress.GetValues().ToArray();
- reports.Should().NotBeEmpty();
- reports.Should().AllSatisfy(v => v.Should().BeInRange(0.0, 1.0));
- reports[^1].Should().BeApproximately(1.0, precision: 1e-5);
- }
}
From b095af14fb7c5ad9d7261fe208db59c0e4ccbb8e Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Sat, 11 Apr 2026 22:01:42 +0300
Subject: [PATCH 34/43] Update PowerKit/Extensions/StreamExtensions.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
PowerKit/Extensions/StreamExtensions.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/PowerKit/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs
index de41fd5..e392e83 100644
--- a/PowerKit/Extensions/StreamExtensions.cs
+++ b/PowerKit/Extensions/StreamExtensions.cs
@@ -82,7 +82,9 @@ public async ValueTask CopyToAsync(
)
{
var contentLength = source.CanSeek ? source.Length : -1;
- await source.CopyToAsync(destination, contentLength, progress, cancellationToken);
+ await source
+ .CopyToAsync(destination, contentLength, progress, cancellationToken)
+ .ConfigureAwait(false);
}
}
}
From b79b77c2b784252ab91ec2306e9357198b031691 Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Sat, 11 Apr 2026 22:02:47 +0300
Subject: [PATCH 35/43] Update
PowerKit/Extensions/AggregateExceptionExtensions.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
PowerKit/Extensions/AggregateExceptionExtensions.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/PowerKit/Extensions/AggregateExceptionExtensions.cs b/PowerKit/Extensions/AggregateExceptionExtensions.cs
index b0aaaaf..00ca0c9 100644
--- a/PowerKit/Extensions/AggregateExceptionExtensions.cs
+++ b/PowerKit/Extensions/AggregateExceptionExtensions.cs
@@ -10,7 +10,7 @@ internal static class AggregateExceptionExtensions
public Exception? TryGetSingle()
{
var exceptions = exception.Flatten().InnerExceptions;
- return exceptions.Count == 1 ? exceptions.Single() : null;
+ return exceptions.Count == 1 ? exceptions[0] : null;
}
}
}
From caedc2914757b58d7fbd2311895e11ccd6d51873 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 12 Apr 2026 09:44:25 +0000
Subject: [PATCH 36/43] Expand InvalidFileNameChars to include ASCII control
characters 0x01-0x1F and add braces to foreach
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/a2731348-f326-4da6-af04-9d444f6f2efd
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/PathExtensionsTests.cs | 1 +
PowerKit/Extensions/PathExtensions.cs | 6 ++++++
2 files changed, 7 insertions(+)
diff --git a/PowerKit.Tests/PathExtensionsTests.cs b/PowerKit.Tests/PathExtensionsTests.cs
index 04fd106..b812716 100644
--- a/PowerKit.Tests/PathExtensionsTests.cs
+++ b/PowerKit.Tests/PathExtensionsTests.cs
@@ -16,6 +16,7 @@ public void EscapeFileName_Test()
Path.EscapeFileName("a\\b").Should().Be("a_b");
Path.EscapeFileName("C:drive").Should().Be("C_drive");
Path.EscapeFileName("a\0b/c\\d:e*f?g\"h InvalidFileNameChars =
[
'\0', // Null character - invalid on all filesystems
+ '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // ASCII control characters -
+ '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', // invalid on Windows
+ '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', // (NTFS/FAT32)
+ '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F',
'/', // Path separator on Unix and Windows
'\\', // Path separator on Windows
':', // Reserved on Windows (drive letters, NTFS streams)
@@ -31,7 +35,9 @@ public static string EscapeFileName(string fileName)
var buffer = new StringBuilder(fileName.Length);
foreach (var c in fileName)
+ {
buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
+ }
// File names cannot end with a dot or whitespace (invalid on Windows, ambiguous on other filesystems)
while (buffer.Length > 0 && (buffer[buffer.Length - 1] == '.' || char.IsWhiteSpace(buffer[buffer.Length - 1])))
From 0c1376cf97b894180d3027a141a3f67f53979bc8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 12 Apr 2026 09:50:00 +0000
Subject: [PATCH 37/43] Rename char variable 'c' to 'ch' in PathExtensions and
StringExtensions
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/bb331d67-d827-44d6-825d-c79bf921f897
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/Extensions/PathExtensions.cs | 4 ++--
PowerKit/Extensions/StringExtensions.cs | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index 0f06e3a..5800651 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -34,9 +34,9 @@ public static string EscapeFileName(string fileName)
{
var buffer = new StringBuilder(fileName.Length);
- foreach (var c in fileName)
+ foreach (var ch in fileName)
{
- buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
+ buffer.Append(!InvalidFileNameChars.Contains(ch) ? ch : '_');
}
// File names cannot end with a dot or whitespace (invalid on Windows, ambiguous on other filesystems)
diff --git a/PowerKit/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs
index c6ad9d8..57f6c66 100644
--- a/PowerKit/Extensions/StringExtensions.cs
+++ b/PowerKit/Extensions/StringExtensions.cs
@@ -33,14 +33,14 @@ public string SeparateWords(char separator)
{
var builder = new StringBuilder(str.Length * 2);
- foreach (var c in str)
+ foreach (var ch in str)
{
- if (char.IsUpper(c) && builder.Length > 0)
+ if (char.IsUpper(ch) && builder.Length > 0)
{
builder.Append(separator);
}
- builder.Append(c);
+ builder.Append(ch);
}
return builder.ToString();
From fa5303316753493d9292df853099af5f5ee1af54 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 12 Apr 2026 10:07:00 +0000
Subject: [PATCH 38/43] Address PR review comments: pattern matching in
AggregateExceptionExtensions, braces in AsyncEnumerableExtensions, add
SubstringUntilLast/SubstringAfterLast, refactor PathExtensions with
GetInvalidFileNameChars/GetInvalidPathChars/crossPlatform param
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/f860d5c2-2b94-48ae-a785-334b01ce9d09
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit.Tests/PathExtensionsTests.cs | 27 +++++++++++
PowerKit.Tests/StringExtensionsTests.cs | 18 ++++++++
.../AggregateExceptionExtensions.cs | 8 +---
.../Extensions/AsyncEnumerableExtensions.cs | 2 +
PowerKit/Extensions/PathExtensions.cs | 45 ++++++++++++++++---
PowerKit/Extensions/StringExtensions.cs | 20 +++++++++
6 files changed, 107 insertions(+), 13 deletions(-)
diff --git a/PowerKit.Tests/PathExtensionsTests.cs b/PowerKit.Tests/PathExtensionsTests.cs
index b812716..44efd7d 100644
--- a/PowerKit.Tests/PathExtensionsTests.cs
+++ b/PowerKit.Tests/PathExtensionsTests.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Linq;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
@@ -7,6 +8,32 @@ namespace PowerKit.Tests;
public class PathExtensionsTests
{
+ [Fact]
+ public void GetInvalidFileNameChars_Test()
+ {
+ // Act & assert
+ Path.GetInvalidFileNameChars(crossPlatform: true).Should().Contain('/');
+ Path.GetInvalidFileNameChars(crossPlatform: true).Should().Contain('\\');
+ Path.GetInvalidFileNameChars(crossPlatform: true).Should().Contain('\0');
+ Path.GetInvalidFileNameChars(crossPlatform: true).Should().Contain('\x01');
+ Path.GetInvalidFileNameChars(crossPlatform: false)
+ .Should()
+ .BeEquivalentTo(Path.GetInvalidFileNameChars());
+ }
+
+ [Fact]
+ public void GetInvalidPathChars_Test()
+ {
+ // Act & assert
+ Path.GetInvalidPathChars(crossPlatform: true).Should().Contain('\0');
+ Path.GetInvalidPathChars(crossPlatform: true).Should().Contain('|');
+ Path.GetInvalidPathChars(crossPlatform: true).Should().NotContain('/');
+ Path.GetInvalidPathChars(crossPlatform: true).Should().NotContain('\\');
+ Path.GetInvalidPathChars(crossPlatform: false)
+ .Should()
+ .BeEquivalentTo(Path.GetInvalidPathChars());
+ }
+
[Fact]
public void EscapeFileName_Test()
{
diff --git a/PowerKit.Tests/StringExtensionsTests.cs b/PowerKit.Tests/StringExtensionsTests.cs
index 8b0b4a0..a60e97a 100644
--- a/PowerKit.Tests/StringExtensionsTests.cs
+++ b/PowerKit.Tests/StringExtensionsTests.cs
@@ -15,6 +15,15 @@ public void SubstringUntil_Test()
"xhello".SubstringUntil("x").Should().Be("");
}
+ [Fact]
+ public void SubstringUntilLast_Test()
+ {
+ // Act & assert
+ "hello world foo".SubstringUntilLast(" ").Should().Be("hello world");
+ "hello".SubstringUntilLast("x").Should().Be("hello");
+ "a.b.c".SubstringUntilLast(".").Should().Be("a.b");
+ }
+
[Fact]
public void SubstringAfter_Test()
{
@@ -24,6 +33,15 @@ public void SubstringAfter_Test()
"hellox".SubstringAfter("x").Should().Be("");
}
+ [Fact]
+ public void SubstringAfterLast_Test()
+ {
+ // Act & assert
+ "hello world foo".SubstringAfterLast(" ").Should().Be("foo");
+ "hello".SubstringAfterLast("x").Should().Be("");
+ "a.b.c".SubstringAfterLast(".").Should().Be("c");
+ }
+
[Fact]
public void Truncate_Test()
{
diff --git a/PowerKit/Extensions/AggregateExceptionExtensions.cs b/PowerKit/Extensions/AggregateExceptionExtensions.cs
index 00ca0c9..c0d0fea 100644
--- a/PowerKit/Extensions/AggregateExceptionExtensions.cs
+++ b/PowerKit/Extensions/AggregateExceptionExtensions.cs
@@ -1,5 +1,4 @@
using System;
-using System.Linq;
namespace PowerKit.Extensions;
@@ -7,10 +6,7 @@ internal static class AggregateExceptionExtensions
{
extension(AggregateException exception)
{
- public Exception? TryGetSingle()
- {
- var exceptions = exception.Flatten().InnerExceptions;
- return exceptions.Count == 1 ? exceptions[0] : null;
- }
+ public Exception? TryGetSingle() =>
+ exception.Flatten().InnerExceptions is [var single] ? single : null;
}
}
diff --git a/PowerKit/Extensions/AsyncEnumerableExtensions.cs b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
index f91ed0e..b7c11b8 100644
--- a/PowerKit/Extensions/AsyncEnumerableExtensions.cs
+++ b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
@@ -46,7 +46,9 @@ var item in source
)
{
foreach (var result in transform(item))
+ {
yield return result;
+ }
}
}
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index 5800651..bc5691e 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -1,16 +1,18 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
namespace PowerKit.Extensions;
internal static class PathExtensions
{
- // This is a union of invalid characters from Windows (NTFS/FAT32), Linux (ext4/XFS), and macOS (HFS+/APFS).
- // We use this instead of Path.GetInvalidFileNameChars() because that only returns OS-specific characters,
- // not filesystem-specific characters. It's possible to use, for example, an NTFS drive on Linux,
- // which would make some additional characters invalid that are otherwise valid on Linux.
- private static readonly HashSet InvalidFileNameChars =
+ // Characters that are invalid in file names across all major filesystems
+ // (Windows NTFS/FAT32, Linux ext4/XFS, macOS HFS+/APFS), beyond what
+ // the OS-specific Path.GetInvalidFileNameChars() returns.
+ // This is useful when working with files that may be accessed from
+ // different operating systems, such as NTFS drives on Linux.
+ private static readonly HashSet CrossPlatformInvalidFileNameChars =
[
'\0', // Null character - invalid on all filesystems
'\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // ASCII control characters -
@@ -28,15 +30,44 @@ internal static class PathExtensions
'|', // Pipe on Windows
];
+ private static readonly HashSet CrossPlatformInvalidPathChars =
+ [
+ '\0', // Null character - invalid on all filesystems
+ '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // ASCII control characters -
+ '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', // invalid on Windows
+ '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', // (NTFS/FAT32)
+ '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F',
+ '*', // Wildcard on Windows
+ '?', // Wildcard on Windows
+ '"', // Reserved on Windows
+ '<', // Redirection on Windows
+ '>', // Redirection on Windows
+ '|', // Pipe on Windows
+ ];
+
extension(Path)
{
- public static string EscapeFileName(string fileName)
+ public static char[] GetInvalidFileNameChars(bool crossPlatform) =>
+ crossPlatform
+ ? CrossPlatformInvalidFileNameChars.ToArray()
+ : Path.GetInvalidFileNameChars();
+
+ public static char[] GetInvalidPathChars(bool crossPlatform) =>
+ crossPlatform
+ ? CrossPlatformInvalidPathChars.ToArray()
+ : Path.GetInvalidPathChars();
+
+ public static string EscapeFileName(string fileName, bool crossPlatform = true)
{
+ var invalidChars = crossPlatform
+ ? (IReadOnlyCollection)CrossPlatformInvalidFileNameChars
+ : Path.GetInvalidFileNameChars();
+
var buffer = new StringBuilder(fileName.Length);
foreach (var ch in fileName)
{
- buffer.Append(!InvalidFileNameChars.Contains(ch) ? ch : '_');
+ buffer.Append(!invalidChars.Contains(ch) ? ch : '_');
}
// File names cannot end with a dot or whitespace (invalid on Windows, ambiguous on other filesystems)
diff --git a/PowerKit/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs
index 57f6c66..2320da9 100644
--- a/PowerKit/Extensions/StringExtensions.cs
+++ b/PowerKit/Extensions/StringExtensions.cs
@@ -17,6 +17,16 @@ public string SubstringUntil(
_ => str,
};
+ public string SubstringUntilLast(
+ string sub,
+ StringComparison comparison = StringComparison.Ordinal
+ ) =>
+ str.LastIndexOf(sub, comparison) switch
+ {
+ >= 0 and var index => str[..index],
+ _ => str,
+ };
+
public string SubstringAfter(
string sub,
StringComparison comparison = StringComparison.Ordinal
@@ -27,6 +37,16 @@ public string SubstringAfter(
_ => "",
};
+ public string SubstringAfterLast(
+ string sub,
+ StringComparison comparison = StringComparison.Ordinal
+ ) =>
+ str.LastIndexOf(sub, comparison) switch
+ {
+ >= 0 and var index => str[(index + sub.Length)..],
+ _ => "",
+ };
+
public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str;
public string SeparateWords(char separator)
From afbe49ace3e74ff16713afa354fb84751f2f81a5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 12 Apr 2026 10:09:41 +0000
Subject: [PATCH 39/43] Cache cross-platform invalid char arrays to avoid
repeated allocations
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/f860d5c2-2b94-48ae-a785-334b01ce9d09
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/Extensions/PathExtensions.cs | 31 ++++++++++++++++-----------
1 file changed, 19 insertions(+), 12 deletions(-)
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index bc5691e..2065519 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -1,18 +1,12 @@
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Text;
namespace PowerKit.Extensions;
internal static class PathExtensions
{
- // Characters that are invalid in file names across all major filesystems
- // (Windows NTFS/FAT32, Linux ext4/XFS, macOS HFS+/APFS), beyond what
- // the OS-specific Path.GetInvalidFileNameChars() returns.
- // This is useful when working with files that may be accessed from
- // different operating systems, such as NTFS drives on Linux.
- private static readonly HashSet CrossPlatformInvalidFileNameChars =
+ private static readonly char[] CrossPlatformInvalidFileNameCharsArray =
[
'\0', // Null character - invalid on all filesystems
'\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // ASCII control characters -
@@ -30,7 +24,16 @@ internal static class PathExtensions
'|', // Pipe on Windows
];
- private static readonly HashSet CrossPlatformInvalidPathChars =
+ // Characters that are invalid in file names across all major filesystems
+ // (Windows NTFS/FAT32, Linux ext4/XFS, macOS HFS+/APFS), beyond what
+ // the OS-specific Path.GetInvalidFileNameChars() returns.
+ // This is useful when working with files that may be accessed from
+ // different operating systems, such as NTFS drives on Linux.
+ private static readonly HashSet CrossPlatformInvalidFileNameChars = new(
+ CrossPlatformInvalidFileNameCharsArray
+ );
+
+ private static readonly char[] CrossPlatformInvalidPathCharsArray =
[
'\0', // Null character - invalid on all filesystems
'\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // ASCII control characters -
@@ -45,23 +48,27 @@ internal static class PathExtensions
'|', // Pipe on Windows
];
+ private static readonly HashSet CrossPlatformInvalidPathChars = new(
+ CrossPlatformInvalidPathCharsArray
+ );
+
extension(Path)
{
public static char[] GetInvalidFileNameChars(bool crossPlatform) =>
crossPlatform
- ? CrossPlatformInvalidFileNameChars.ToArray()
+ ? CrossPlatformInvalidFileNameCharsArray
: Path.GetInvalidFileNameChars();
public static char[] GetInvalidPathChars(bool crossPlatform) =>
crossPlatform
- ? CrossPlatformInvalidPathChars.ToArray()
+ ? CrossPlatformInvalidPathCharsArray
: Path.GetInvalidPathChars();
public static string EscapeFileName(string fileName, bool crossPlatform = true)
{
var invalidChars = crossPlatform
- ? (IReadOnlyCollection)CrossPlatformInvalidFileNameChars
- : Path.GetInvalidFileNameChars();
+ ? CrossPlatformInvalidFileNameChars
+ : new HashSet(Path.GetInvalidFileNameChars());
var buffer = new StringBuilder(fileName.Length);
From 3891af9b88919ef9a47e02873a7266a813836a2d Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Sun, 12 Apr 2026 13:23:52 +0300
Subject: [PATCH 40/43] Update PowerKit/Extensions/AsyncEnumerableExtensions.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
PowerKit/Extensions/AsyncEnumerableExtensions.cs | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/PowerKit/Extensions/AsyncEnumerableExtensions.cs b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
index b7c11b8..b3ac565 100644
--- a/PowerKit/Extensions/AsyncEnumerableExtensions.cs
+++ b/PowerKit/Extensions/AsyncEnumerableExtensions.cs
@@ -20,16 +20,14 @@ public async IAsyncEnumerable TakeAsync(
var currentCount = 0;
- await foreach (
- var item in source
- .WithCancellation(cancellationToken)
- .ConfigureAwait(false)
+ await using var enumerator = source.GetAsyncEnumerator(cancellationToken);
+
+ while (
+ currentCount < count
+ && await enumerator.MoveNextAsync().ConfigureAwait(false)
)
{
- if (currentCount >= count)
- yield break;
-
- yield return item;
+ yield return enumerator.Current;
currentCount++;
}
}
From b082066437a86f668a1718ef1b7c6207d330d464 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 12 Apr 2026 10:29:05 +0000
Subject: [PATCH 41/43] Derive CrossPlatformInvalidPathChars from
CrossPlatformInvalidFileNameChars to reduce duplication
Agent-Logs-Url: https://github.com/Tyrrrz/PowerKit/sessions/56e3e0eb-b42d-49cc-a361-0b7b771dd982
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
---
PowerKit/Extensions/PathExtensions.cs | 19 ++++++-------------
1 file changed, 6 insertions(+), 13 deletions(-)
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index 2065519..2884b17 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
namespace PowerKit.Extensions;
@@ -33,20 +34,12 @@ internal static class PathExtensions
CrossPlatformInvalidFileNameCharsArray
);
+ // Path chars are the same as file name chars, except path separators
+ // and the colon (drive letter separator) are valid in paths.
private static readonly char[] CrossPlatformInvalidPathCharsArray =
- [
- '\0', // Null character - invalid on all filesystems
- '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // ASCII control characters -
- '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', // invalid on Windows
- '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', // (NTFS/FAT32)
- '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F',
- '*', // Wildcard on Windows
- '?', // Wildcard on Windows
- '"', // Reserved on Windows
- '<', // Redirection on Windows
- '>', // Redirection on Windows
- '|', // Pipe on Windows
- ];
+ CrossPlatformInvalidFileNameCharsArray
+ .Where(ch => ch != '/' && ch != '\\' && ch != ':')
+ .ToArray();
private static readonly HashSet CrossPlatformInvalidPathChars = new(
CrossPlatformInvalidPathCharsArray
From 381cb5f68332ac46f02bc10b7e5fec16924f8dcf Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Sun, 12 Apr 2026 13:40:43 +0300
Subject: [PATCH 42/43] Update PathExtensions.cs
---
PowerKit/Extensions/PathExtensions.cs | 38 +++++++++++----------------
1 file changed, 15 insertions(+), 23 deletions(-)
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index 2884b17..5ae9962 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -5,9 +5,14 @@
namespace PowerKit.Extensions;
-internal static class PathExtensions
+file static class PathEx
{
- private static readonly char[] CrossPlatformInvalidFileNameCharsArray =
+ // Characters that are invalid in file names across all major filesystems
+ // (Windows NTFS/FAT32, Linux ext4/XFS, macOS HFS+/APFS), beyond what
+ // the OS-specific Path.GetInvalidFileNameChars() returns.
+ // This is useful when working with files that may be accessed from
+ // different operating systems, such as NTFS drives on Linux.
+ private static readonly char[] CrossPlatformInvalidFileNameChars =
[
'\0', // Null character - invalid on all filesystems
'\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // ASCII control characters -
@@ -25,44 +30,31 @@ internal static class PathExtensions
'|', // Pipe on Windows
];
- // Characters that are invalid in file names across all major filesystems
- // (Windows NTFS/FAT32, Linux ext4/XFS, macOS HFS+/APFS), beyond what
- // the OS-specific Path.GetInvalidFileNameChars() returns.
- // This is useful when working with files that may be accessed from
- // different operating systems, such as NTFS drives on Linux.
- private static readonly HashSet CrossPlatformInvalidFileNameChars = new(
- CrossPlatformInvalidFileNameCharsArray
- );
-
// Path chars are the same as file name chars, except path separators
// and the colon (drive letter separator) are valid in paths.
- private static readonly char[] CrossPlatformInvalidPathCharsArray =
- CrossPlatformInvalidFileNameCharsArray
+ private static readonly char[] CrossPlatformInvalidPathChars =
+ CrossPlatformInvalidFileNameChars
.Where(ch => ch != '/' && ch != '\\' && ch != ':')
.ToArray();
+}
- private static readonly HashSet CrossPlatformInvalidPathChars = new(
- CrossPlatformInvalidPathCharsArray
- );
-
+internal static class PathExtensions
+{
extension(Path)
{
public static char[] GetInvalidFileNameChars(bool crossPlatform) =>
crossPlatform
- ? CrossPlatformInvalidFileNameCharsArray
+ ? PathEx.CrossPlatformInvalidFileNameChars
: Path.GetInvalidFileNameChars();
public static char[] GetInvalidPathChars(bool crossPlatform) =>
crossPlatform
- ? CrossPlatformInvalidPathCharsArray
+ ? PathEx.CrossPlatformInvalidPathChars
: Path.GetInvalidPathChars();
public static string EscapeFileName(string fileName, bool crossPlatform = true)
{
- var invalidChars = crossPlatform
- ? CrossPlatformInvalidFileNameChars
- : new HashSet(Path.GetInvalidFileNameChars());
-
+ var invalidChars = new HashSet(Path.GetInvalidFileNameChars(crossPlatform));
var buffer = new StringBuilder(fileName.Length);
foreach (var ch in fileName)
From d81f728d8585363c4e3399989b8646f3594b0e36 Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Sun, 12 Apr 2026 13:41:29 +0300
Subject: [PATCH 43/43] Update PathExtensions.cs
---
PowerKit/Extensions/PathExtensions.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs
index 5ae9962..003ddc7 100644
--- a/PowerKit/Extensions/PathExtensions.cs
+++ b/PowerKit/Extensions/PathExtensions.cs
@@ -12,7 +12,7 @@ file static class PathEx
// the OS-specific Path.GetInvalidFileNameChars() returns.
// This is useful when working with files that may be accessed from
// different operating systems, such as NTFS drives on Linux.
- private static readonly char[] CrossPlatformInvalidFileNameChars =
+ public static readonly char[] CrossPlatformInvalidFileNameChars =
[
'\0', // Null character - invalid on all filesystems
'\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // ASCII control characters -
@@ -32,7 +32,7 @@ file static class PathEx
// Path chars are the same as file name chars, except path separators
// and the colon (drive letter separator) are valid in paths.
- private static readonly char[] CrossPlatformInvalidPathChars =
+ public static readonly char[] CrossPlatformInvalidPathChars =
CrossPlatformInvalidFileNameChars
.Where(ch => ch != '/' && ch != '\\' && ch != ':')
.ToArray();