Skip to content

Commit

Permalink
feat: adds TransferRate extension that wraps the response content to …
Browse files Browse the repository at this point in the history
…introduce artificial slower responses
  • Loading branch information
skwasjer committed Dec 24, 2023
1 parent a6ba8cd commit 0b43c97
Show file tree
Hide file tree
Showing 11 changed files with 503 additions and 10 deletions.
73 changes: 73 additions & 0 deletions src/MockHttp/Extensions/ResponseBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,79 @@ public static IWithResponse Latency(this IWithResponse builder, Func<NetworkLate
return builder.Latency(latency());
}

/// <summary>
/// Limits the response to a specific bit rate to simulate slow(er) network transfer rates.
/// <para>
/// The content stream returned in the response is throttled to match the requested bit rate.
/// </para>
/// </summary>
/// <remarks>
/// - Not 100% accurate (just like real world :p).
/// </remarks>
/// <param name="builder">The builder.</param>
/// <param name="bitRate">The bit rate to simulate.</param>
/// <returns>The builder to continue chaining additional behaviors.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder" /> or <paramref name="bitRate" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the bit rate is less than 128.</exception>
public static IWithResponse TransferRate(this IWithResponse builder, Func<BitRate> bitRate)
{
if (bitRate is null)
{
throw new ArgumentNullException(nameof(bitRate));
}

return builder.TransferRate(bitRate());
}

/// <summary>
/// Limits the response to a specific bit rate to simulate slow(er) network transfer rates.
/// <para>
/// The content stream returned in the response is throttled to match the requested bit rate.
/// </para>
/// </summary>
/// <remarks>
/// - Not 100% accurate (just like real world :p).
/// </remarks>
/// <param name="builder">The builder.</param>
/// <param name="bitRate">The bit rate to simulate.</param>
/// <returns>The builder to continue chaining additional behaviors.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the bit rate is less than 128.</exception>
public static IWithResponse TransferRate(this IWithResponse builder, BitRate bitRate)
{
if (bitRate is null)
{
throw new ArgumentNullException(nameof(bitRate));
}

return builder.TransferRate((int)bitRate);
}

/// <summary>
/// Limits the response to a specific bit rate to simulate slow(er) network transfer rates.
/// <para>
/// The content stream returned in the response is throttled to match the requested bit rate.
/// </para>
/// </summary>
/// <remarks>
/// - Not 100% accurate (just like real world :p).
/// </remarks>
/// <param name="builder">The builder.</param>
/// <param name="bitRate">The bit rate to simulate.</param>
/// <returns>The builder to continue chaining additional behaviors.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder" /> or <paramref name="bitRate" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the bit rate is less than 128.</exception>
public static IWithResponse TransferRate(this IWithResponse builder, int bitRate)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Behaviors.Replace(new TransferRateBehavior(bitRate));
return builder;
}

private static string? ConvertToString<T>(T v)
{
switch (v)
Expand Down
25 changes: 22 additions & 3 deletions src/MockHttp/Language/Flow/Response/ResponseBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,36 @@ public int Compare(IResponseBehavior? x, IResponseBehavior? y)

private static int Compare(IResponseBehavior? x, IResponseBehavior? y, bool flipped)
{
if (ReferenceEquals(x, null))
{
return 1;
}

if (ReferenceEquals(y, null))
{
return -1;
}

if (ReferenceEquals(x, y))
{
return 0;
}

return x switch
{
// The network latency behavior must always come first.
NetworkLatencyBehavior => -1,
_ => CompareOtherWayAround()
// The rate limit behavior must always come first except when the latency behavior is also present.
TransferRateBehavior => y is NetworkLatencyBehavior
? 1
: CompareOtherWayAround(-1),
_ => CompareOtherWayAround(0)
};

int CompareOtherWayAround()
int CompareOtherWayAround(int result)
{
return flipped
? 0
? result
#pragma warning disable S2234 // Parameters to 'Compare' have the same names but not the same order as the method arguments. - justification: intentional.
: -Compare(y, x, true);
#pragma warning restore S2234
Expand Down
149 changes: 149 additions & 0 deletions src/MockHttp/Responses/BitRate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System.Globalization;
using MockHttp.IO;

// ReSharper disable once CheckNamespace
namespace MockHttp;

/// <summary>
/// Defines different types of bit rates to simulate a slow network.
/// </summary>
public sealed class BitRate
{
private readonly Func<int> _factory;
private readonly string _name;

private BitRate(Func<int> factory, string name)
{
_factory = factory;
_name = name;
}

/// <inheritdoc />
public override string ToString()
{
return $"{GetType().Name}.{_name}";
}

/// <summary>
/// 2G (mobile network) bit rate. (~64kbps).
/// </summary>
public static BitRate TwoG()
{
return Create(64_000, nameof(TwoG));
}

/// <summary>
/// 3G (mobile network) bit rate. (~2Mbps)
/// </summary>
public static BitRate ThreeG()
{
return Create(2_000_000, nameof(ThreeG));
}

/// <summary>
/// 4G (mobile network) bit rate. (~64Mbps)
/// </summary>
public static BitRate FourG()
{
return Create(64_000_000, nameof(FourG));
}

/// <summary>
/// 5G (mobile network) bit rate. (~512Mbps)
/// </summary>
public static BitRate FiveG()
{
return Create(512_000_000, nameof(FiveG));
}

/// <summary>
/// 10 Mbps.
/// </summary>
public static BitRate TenMegabit()
{
return Create(10_000_000, nameof(TenMegabit));
}

/// <summary>
/// 100 Mbps.
/// </summary>
public static BitRate OneHundredMegabit()
{
return Create(100_000_000, nameof(OneHundredMegabit));
}

/// <summary>
/// 1 Gbps.
/// </summary>
public static BitRate OneGigabit()
{
return Create(1_000_000_000, nameof(OneGigabit));
}

/// <summary>
/// Converts a bit rate to an integer representing the bit rate in bits per second.
/// </summary>
/// <param name="bitRate"></param>
/// <returns></returns>
public static explicit operator int (BitRate bitRate)
{
return ToInt32(bitRate);
}

/// <summary>
/// Converts a bit rate to an integer representing the bit rate in bits per second.
/// </summary>
/// <param name="bitRate"></param>
/// <returns></returns>
public static explicit operator BitRate (int bitRate)
{
return FromInt32(bitRate);
}

/// <summary>
/// Converts a bit rate to an integer representing the bit rate in bits per second.
/// </summary>
/// <param name="bitRate">The bit rate.</param>
/// <returns>The underlying bit rate value.</returns>
public static int ToInt32(BitRate bitRate)
{
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
return bitRate?._factory() ?? -1;
}

/// <summary>
/// Convert an integer bit rate (in bits per second) to a <see cref="BitRate" />.
/// </summary>
/// <param name="bitRate">The bit rate.</param>
/// <returns>The bit rate.</returns>
public static BitRate FromInt32(int bitRate)
{
return Create(bitRate, FormatBps(bitRate));
}

private static BitRate Create(int bitRate, string name)
{
if (bitRate <= 0)
{
throw new ArgumentOutOfRangeException(nameof(bitRate));
}

return new BitRate(() => bitRate, name);
}

private static string FormatBps(long value)
{
return BpsToString().ToString(CultureInfo.InvariantCulture);

FormattableString BpsToString()
{
return value switch
{
< 1_000 => $"Around({value}bps)",
< 1_000_000 => $"Around({(double)value / 1_000:#.##}kbps)",
< 1_000_000_000 => $"Around({(double)value / 1_000_000:#.##}Mbps)",
_ => $"Around({(double)value / 1_000_000_000:#.##}Gbps)"
};
}
}
}
65 changes: 65 additions & 0 deletions src/MockHttp/Responses/TransferRateBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Net;
using MockHttp.IO;

namespace MockHttp.Responses;

internal sealed class TransferRateBehavior : IResponseBehavior
{
private readonly int _bitRate;

public TransferRateBehavior(int bitRate)
{
if (bitRate < RateLimitedStream.MinBitRate)
{
throw new ArgumentOutOfRangeException(nameof(bitRate), $"Bit rate must be higher than or equal to {RateLimitedStream.MinBitRate}.");
}

_bitRate = bitRate;
}

public async Task HandleAsync(MockHttpRequestContext requestContext, HttpResponseMessage responseMessage, ResponseHandlerDelegate next, CancellationToken cancellationToken)
{
await next(requestContext, responseMessage, cancellationToken).ConfigureAwait(false);
responseMessage.Content = new RateLimitedHttpContent(responseMessage.Content, _bitRate);
}

private sealed class RateLimitedHttpContent : HttpContent
{
private readonly int _bitRate;
private readonly HttpContent _originalContent;

internal RateLimitedHttpContent(HttpContent originalContent, int bitRate)
{
_originalContent = originalContent;
_bitRate = bitRate;
}

protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
Stream originalStream = await _originalContent.ReadAsStreamAsync().ConfigureAwait(false);
var rateLimitedStream = new RateLimitedStream(originalStream, _bitRate);
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
await using (originalStream)
await using (rateLimitedStream)
#else
using (originalStream)
using (rateLimitedStream)
#endif
{
await rateLimitedStream.CopyToAsync(stream).ConfigureAwait(false);
}
}

protected override bool TryComputeLength(out long length)
{
long? contentLength = _originalContent.Headers.ContentLength;
length = 0;
if (contentLength.HasValue)
{
length = contentLength.Value;
}

return contentLength.HasValue;
}
}
}
15 changes: 15 additions & 0 deletions test/MockHttp.Tests/Language/Flow/Response/NullBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ public void Given_null_argument_when_executing_method_it_should_throw(params obj
responseBuilder,
new XDocument(),
(XmlWriterSettings?)null
),
DelegateTestCase.Create(
ResponseBuilderExtensions.TransferRate,
responseBuilder,
BitRate.FourG()
),
DelegateTestCase.Create(
ResponseBuilderExtensions.TransferRate,
responseBuilder,
BitRate.FourG
),
DelegateTestCase.Create(
ResponseBuilderExtensions.TransferRate,
responseBuilder,
(int)BitRate.FourG()
)
};

Expand Down
Loading

0 comments on commit 0b43c97

Please sign in to comment.