Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1e6575e
feat: add Stream.CopyTo/CopyToAsync extensions with progress reporting
Copilot Apr 19, 2026
498a0af
feat: add HttpContent.CopyToAsync extension and tests for Stream/Http…
Copilot Apr 19, 2026
6f8f63c
fix: pass cancellationToken to ReadAsStreamAsync in HttpContentExtens…
Copilot Apr 19, 2026
fac8335
feat: add streamLength overloads to Stream.CopyTo and Stream.CopyToAsync
Copilot Apr 19, 2026
b21ba2e
feat: add ReadAsByteArrayAsync/ReadAsStringAsync to HttpContent and G…
Copilot Apr 19, 2026
3abc455
Add DownloadAsync overloads to HttpClientExtensions
Copilot Apr 19, 2026
6b65775
Move HTTP/stream extensions to Gress.Http namespace
Copilot Apr 19, 2026
9d77be6
Add DownloadAsync file-path overloads to HttpClientExtensions
Copilot Apr 19, 2026
16f3116
Use TempFile from PowerKit in file-download tests
Copilot Apr 19, 2026
913e849
Replace FakeHttpMessageHandler with real HTTP requests to example.com
Copilot Apr 19, 2026
ced4b90
Extract TestUrl constant; use IClassFixture for shared HttpClient
Copilot Apr 19, 2026
0026a3f
Remove null tests; only test string overloads; delegate string to Uri…
Copilot Apr 19, 2026
befdb88
Inline HttpClient in HttpClientSpecs, remove Fixture/IClassFixture
Copilot Apr 19, 2026
eff8a44
Inline HttpClient and URL directly into each test method
Copilot Apr 19, 2026
86a1a83
Update HttpClientSpecs.cs
Tyrrrz Apr 19, 2026
e4330dc
Apply suggestion from @Copilot
Tyrrrz Apr 19, 2026
684aa79
Remove HttpContentSpecs.cs — covered by HttpClient tests via delegation
Copilot Apr 19, 2026
78dbcdd
Update Gress/Http/HttpClientExtensions.cs
Tyrrrz Apr 19, 2026
ad91080
Merge top 2 stream tests + HttpClient tests into ExtensionSpecs; remo…
Copilot Apr 19, 2026
61353fe
Use UriKind.RelativeOrAbsolute in all string-overload Uri constructions
Copilot Apr 19, 2026
ce511e8
Remove unpredictable progress assertions from HttpClient tests hittin…
Copilot Apr 19, 2026
71898ff
Use https://github.com/Tyrrrz/Gress in HttpClient tests; restore prog…
Copilot Apr 19, 2026
dc22050
Update HttpClientExtensions.cs
Tyrrrz Apr 19, 2026
b7c8cf4
Use ArrayPool and Random to fill stream test data
Copilot Apr 19, 2026
04bed4e
Add using var, ConfigureAwait(false), FileStream async options, reord…
Copilot Apr 19, 2026
bdbb9f4
refactor: use RentOwner in tests, ArrayPool+pattern matching in Strea…
Copilot Apr 19, 2026
3f15226
fix: use buffer.Length instead of DefaultBufferSize in ArrayPool read…
Copilot Apr 19, 2026
92d00f8
Update HttpClientExtensions.cs
Tyrrrz Apr 19, 2026
82f4366
Update PowerKit to v1.0.0 and fix ISpanOwner API usage
Copilot Apr 19, 2026
185abd2
Use ISpanOwner.Span API from PowerKit v1.0.0 in ExtensionSpecs
Copilot Apr 19, 2026
ac81aae
Update HttpContentExtensions.cs
Tyrrrz Apr 19, 2026
f8137d0
Update HttpContentExtensions.cs
Tyrrrz Apr 19, 2026
ce898dd
Add await to string delegating overloads in HttpClientExtensions; use…
Copilot Apr 19, 2026
b356be9
asd
Tyrrrz Apr 19, 2026
535d58c
asd
Tyrrrz Apr 19, 2026
e203d97
asd
Tyrrrz Apr 19, 2026
c0261e0
Add readme info
Tyrrrz Apr 19, 2026
c3c137f
Call into PowerKit more
Tyrrrz Apr 20, 2026
197ff8d
Use web resources that report content length in tests
Tyrrrz Apr 20, 2026
ea78627
Boost coverage
Tyrrrz Apr 20, 2026
d7a888f
Refactor
Tyrrrz Apr 20, 2026
6a95d2e
Improve correctness
Tyrrrz Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.14" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.14" />
Expand All @@ -15,8 +14,8 @@
<PackageVersion Include="FluentAssertions" Version="8.9.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="PolyShim" Version="2.9.0" />
<PackageVersion Include="PowerKit" Version="0.0.0-a.3" />
<PackageVersion Include="PolyShim" Version="2.11.0-a.3" />
<PackageVersion Include="PowerKit" Version="1.1.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions Gress.Tests/Gress.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="coverlet.collector" PrivateAssets="all" />
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="PowerKit" PrivateAssets="all" />
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
Expand Down
102 changes: 102 additions & 0 deletions Gress.Tests/IntegrationSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Gress.Integration;
using PowerKit;
using PowerKit.Extensions;
using Xunit;

namespace Gress.Tests;

public class IntegrationSpecs
{
[Fact]
public async Task I_can_copy_a_stream_to_another_stream_with_progress()
{
// Arrange
using var buffer = SpanPool<byte>.Shared.Rent(
// Longer buffer to ensure multiple progress reports
81920 * 2
+ 1000
);

Random.Shared.NextBytes(buffer.Span);
var data = buffer.Span.ToArray();

using var source = new MemoryStream(data);
using var destination = new MemoryStream();
var progress = new ProgressCollector<Percentage>();

// Act
await source.CopyToAsync(destination, progress);

// Assert
destination.ToArray().Should().Equal(data);
progress.GetValues().Should().NotBeEmpty();
progress.GetValues().Should().BeInAscendingOrder();
}

[Fact]
public async Task I_can_download_a_web_resource_as_a_byte_array_with_progress()
{
// Arrange
using var http = new HttpClient();
var progress = new ProgressCollector<Percentage>();

// Act
var result = await http.GetByteArrayAsync(
// Need something that reports content length
"https://github.com/Tyrrrz/CliWrap/releases/download/3.10.1/CliWrap.3.10.1.nupkg",
progress
);
Comment thread
Tyrrrz marked this conversation as resolved.

// Assert
result.Should().NotBeNullOrEmpty();
progress.GetValues().Should().NotBeEmpty();
}

[Fact]
public async Task I_can_download_a_web_resource_as_a_string_with_progress()
{
// Arrange
using var http = new HttpClient();
var progress = new ProgressCollector<Percentage>();

// Act
var result = await http.GetStringAsync(
// Need something that reports content length
"https://github.com/Tyrrrz/CliWrap/releases/download/3.10.1/CliWrap.3.10.1.nupkg",
progress
);

// Assert
result.Should().NotBeNullOrEmpty();
progress.GetValues().Should().NotBeEmpty();
}

[Fact]
public async Task I_can_download_a_web_resource_to_a_file_with_progress()
{
// Arrange
using var http = new HttpClient();
using var tempFile = TempFile.Create();
var progress = new ProgressCollector<Percentage>();

// Act
await http.DownloadAsync(
// Need something that reports content length
"https://github.com/Tyrrrz/CliWrap/releases/download/3.10.1/CliWrap.3.10.1.nupkg",
tempFile.Path,
progress
);
Comment thread
Tyrrrz marked this conversation as resolved.

// Assert
new FileInfo(tempFile.Path)
.Length.Should()
.BeGreaterThan(0);

progress.GetValues().Should().NotBeEmpty();
}
}
120 changes: 120 additions & 0 deletions Gress/Integration/HttpClientExtensions.cs
Comment thread
Tyrrrz marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using PowerKit.Extensions;

namespace Gress.Integration;

/// <summary>
/// Provides progress-aware extensions for <see cref="HttpClient" />.
/// </summary>
public static class HttpClientExtensions
{
/// <inheritdoc cref="HttpClientExtensions" />
extension(HttpClient client)
{
/// <summary>
/// Sends a GET request and returns the response body as a byte array.
/// </summary>
public async Task<byte[]> GetByteArrayAsync(
Uri requestUri,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
using var response = await client
.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);

response.EnsureSuccessStatusCode();

return await response
.Content.ReadAsByteArrayAsync(progress, cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Sends a GET request and returns the response body as a byte array.
/// </summary>
public async Task<byte[]> GetByteArrayAsync(
string requestUri,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await client
.GetByteArrayAsync(
new Uri(requestUri, UriKind.RelativeOrAbsolute),
progress,
cancellationToken
)
.ConfigureAwait(false);

/// <summary>
/// Sends a GET request and returns the response body as text.
/// </summary>
public async Task<string> GetStringAsync(
Uri requestUri,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
using var response = await client
.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);

response.EnsureSuccessStatusCode();

return await response
.Content.ReadAsStringAsync(progress, cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Sends a GET request and returns the response body as text.
/// </summary>
public async Task<string> GetStringAsync(
string requestUri,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await client
.GetStringAsync(
new Uri(requestUri, UriKind.RelativeOrAbsolute),
progress,
cancellationToken
)
.ConfigureAwait(false);

/// <summary>
/// Sends a GET request and saves the response body to a file.
/// </summary>
public async Task DownloadAsync(
Uri requestUri,
string filePath,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await client
.DownloadAsync(requestUri, filePath, progress?.ToDoubleBased(), cancellationToken)
.ConfigureAwait(false);

/// <summary>
/// Sends a GET request and saves the response body to a file.
/// </summary>
public async Task DownloadAsync(
string requestUri,
string filePath,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await client
.DownloadAsync(
new Uri(requestUri, UriKind.RelativeOrAbsolute),
filePath,
progress,
cancellationToken
)
.ConfigureAwait(false);
}
}
92 changes: 92 additions & 0 deletions Gress/Integration/HttpContentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using PowerKit.Extensions;

namespace Gress.Integration;

/// <summary>
/// Provides progress-aware extensions for <see cref="HttpContent" />.
/// </summary>
public static class HttpContentExtensions
{
/// <inheritdoc cref="HttpContentExtensions" />
extension(HttpContent content)
{
/// <summary>
/// Serializes the HTTP content and writes it to the specified stream.
/// </summary>
public async Task CopyToAsync(
Comment thread
Tyrrrz marked this conversation as resolved.
Stream destination,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await content
.CopyToStreamAsync(destination, progress?.ToDoubleBased(), cancellationToken)
Comment thread
Tyrrrz marked this conversation as resolved.
.ConfigureAwait(false);

private async Task<MemoryStream> ReadAsMemoryStreamAsync(
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
var destination = new MemoryStream();

await content
.CopyToAsync(destination, progress, cancellationToken)
.ConfigureAwait(false);

destination.Position = 0;

return destination;
}

/// <summary>
/// Reads the HTTP content and returns it as a byte array.
/// </summary>
public async Task<byte[]> ReadAsByteArrayAsync(
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
using var buffer = await content
.ReadAsMemoryStreamAsync(progress, cancellationToken)
.ConfigureAwait(false);

return buffer.ToArray();
}

/// <summary>
/// Reads the HTTP content and returns it as a string.
/// </summary>
public async Task<string> ReadAsStringAsync(
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
)
{
var encoding =
content
.Headers.ContentType?.CharSet?.Pipe(c =>
Encoding
.GetEncodings()
.FirstOrDefault(e =>
string.Equals(e.Name, c, StringComparison.OrdinalIgnoreCase)
)
)
?.GetEncoding()
?? Encoding.UTF8;

using var buffer = await content
.ReadAsMemoryStreamAsync(progress, cancellationToken)
.ConfigureAwait(false);

using var reader = new StreamReader(buffer, encoding, true);

return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
}
}
}
52 changes: 52 additions & 0 deletions Gress/Integration/StreamExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using PowerKit.Extensions;

namespace Gress.Integration;

/// <summary>
/// Provides progress-aware extensions for <see cref="Stream" />.
/// </summary>
public static class StreamExtensions
{
/// <inheritdoc cref="StreamExtensions" />
extension(Stream source)
{
/// <summary>
/// Asynchronously copies bytes from the source stream to the destination stream.
/// </summary>
public async Task CopyToAsync(
Stream destination,
long sourceLength,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await source
.CopyToAsync(
destination,
sourceLength,
progress?.ToDoubleBased(),
cancellationToken
)
.ConfigureAwait(false);

/// <summary>
/// Asynchronously copies bytes from the source stream to the destination stream.
/// </summary>
public async Task CopyToAsync(
Stream destination,
IProgress<Percentage>? progress,
CancellationToken cancellationToken = default
) =>
await source
.CopyToAsync(
destination,
source.CanSeek ? source.Length : -1,
progress,
cancellationToken
)
.ConfigureAwait(false);
}
}
Loading