diff --git a/Source/aweXpect.Core/Core/SpanWrapper.cs b/Source/aweXpect.Core/Core/SpanWrapper.cs new file mode 100644 index 000000000..f80fcab58 --- /dev/null +++ b/Source/aweXpect.Core/Core/SpanWrapper.cs @@ -0,0 +1,69 @@ +#if NET8_0_OR_GREATER +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace aweXpect.Core; + +/// +/// Wraps a span by providing access to the underlying data. +/// +public class SpanWrapper : ICollection +{ + private readonly T[] _value; + + /// + public SpanWrapper(ReadOnlySpan span) + { + _value = span.ToArray(); + } + + /// + public SpanWrapper(Span span) + { + _value = span.ToArray(); + } + + /// + /// Creates a new span over the wrapped array. + /// + public Span AsSpan() => _value.AsSpan(); + + /// + public IEnumerator GetEnumerator() + => (_value as IEnumerable).GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + public void Add(T item) + => throw new NotSupportedException("You may not change a SpanWrapper!"); + + /// + public void Clear() + => throw new NotSupportedException("You may not change a SpanWrapper!"); + + /// + public bool Contains(T item) + => _value.Contains(item); + + /// + public void CopyTo(T[] array, int arrayIndex) + => _value.CopyTo(array, arrayIndex); + + /// + public bool Remove(T item) + => throw new NotSupportedException("You may not change a SpanWrapper!"); + + /// + public int Count + => _value.Length; + + /// + public bool IsReadOnly + => true; +} +#endif diff --git a/Source/aweXpect.Core/Expect.cs b/Source/aweXpect.Core/Expect.cs index 3e26cc57e..bd06fbf69 100644 --- a/Source/aweXpect.Core/Expect.cs +++ b/Source/aweXpect.Core/Expect.cs @@ -38,6 +38,26 @@ public static IThat That(Task subject, => new ThatSubject(new ExpectationBuilder( new AsyncValueSource(subject), doNotPopulateThisValue)); +#if NET8_0_OR_GREATER + /// + /// Specify expectations for the current . + /// + public static IThat> That(ReadOnlySpan subject, + [CallerArgumentExpression("subject")] string doNotPopulateThisValue = "") + => new ThatSubject>(new ExpectationBuilder>( + new ValueSource>(new SpanWrapper(subject)), doNotPopulateThisValue)); +#endif + +#if NET8_0_OR_GREATER + /// + /// Specify expectations for the current . + /// + public static IThat> That(Span subject, + [CallerArgumentExpression("subject")] string doNotPopulateThisValue = "") + => new ThatSubject>(new ExpectationBuilder>( + new ValueSource>(new SpanWrapper(subject)), doNotPopulateThisValue)); +#endif + #if NET8_0_OR_GREATER /// /// Specify expectations for the current asynchronous . diff --git a/Source/aweXpect/Results/IsParsableResult.cs b/Source/aweXpect/Results/IsParsableResult.cs index 822603d04..a9d35e48b 100644 --- a/Source/aweXpect/Results/IsParsableResult.cs +++ b/Source/aweXpect/Results/IsParsableResult.cs @@ -5,7 +5,8 @@ namespace aweXpect.Results; /// -/// The result for verifying that the subject is parsable into . +/// The result for verifying that the subject is parsable +/// into . /// public class IsParsableResult( ExpectationBuilder expectationBuilder, diff --git a/Source/aweXpect/Results/IsSpanParsableResult.cs b/Source/aweXpect/Results/IsSpanParsableResult.cs new file mode 100644 index 000000000..405710942 --- /dev/null +++ b/Source/aweXpect/Results/IsSpanParsableResult.cs @@ -0,0 +1,35 @@ +#if NET8_0_OR_GREATER +using System; +using aweXpect.Core; + +namespace aweXpect.Results; + +/// +/// The result for verifying that the subject is parsable +/// into . +/// +public class IsSpanParsableResult( + ExpectationBuilder expectationBuilder, + IThat> subject, + IFormatProvider? formatProvider) + : AndOrResult, IThat>>(expectationBuilder, subject) + where TType : ISpanParsable +{ + private readonly ExpectationBuilder _expectationBuilder = expectationBuilder; + + /// + /// Gives access to the parsed value. + /// + public IThat Which + => new ThatSubject(_expectationBuilder + .ForWhich, TType?>(d => + { + if (TType.TryParse(d.AsSpan(), formatProvider, out TType? result)) + { + return result; + } + + return default; + }, " which ")); +} +#endif diff --git a/Source/aweXpect/Results/IsUtf8SpanParsableResult.cs b/Source/aweXpect/Results/IsUtf8SpanParsableResult.cs new file mode 100644 index 000000000..c4d3ed08d --- /dev/null +++ b/Source/aweXpect/Results/IsUtf8SpanParsableResult.cs @@ -0,0 +1,35 @@ +#if NET8_0_OR_GREATER +using System; +using aweXpect.Core; + +namespace aweXpect.Results; + +/// +/// The result for verifying that the subject is parsable +/// into . +/// +public class IsUtf8SpanParsableResult( + ExpectationBuilder expectationBuilder, + IThat> subject, + IFormatProvider? formatProvider) + : AndOrResult, IThat>>(expectationBuilder, subject) + where TType : IUtf8SpanParsable +{ + private readonly ExpectationBuilder _expectationBuilder = expectationBuilder; + + /// + /// Gives access to the parsed value. + /// + public IThat Which + => new ThatSubject(_expectationBuilder + .ForWhich, TType?>(d => + { + if (TType.TryParse(d.AsSpan(), formatProvider, out TType? result)) + { + return result; + } + + return default; + }, " which ")); +} +#endif diff --git a/Source/aweXpect/That/Spans/ThatSpan.IsParsableInto.cs b/Source/aweXpect/That/Spans/ThatSpan.IsParsableInto.cs new file mode 100644 index 000000000..f8db273f5 --- /dev/null +++ b/Source/aweXpect/That/Spans/ThatSpan.IsParsableInto.cs @@ -0,0 +1,218 @@ +#if NET8_0_OR_GREATER +using System; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Helpers; +using aweXpect.Results; + +namespace aweXpect; + +public static partial class ThatSpan +{ + /// + /// Verifies that the subject is parsable into type . + /// + /// + /// The optional parameter provides culture-specific formatting information + /// in the call to . + /// + public static IsSpanParsableResult IsParsableInto( + this IThat> source, + IFormatProvider? formatProvider = null) + where TType : ISpanParsable + => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsParsableIntoConstraint(it, grammars, formatProvider)), + source, + formatProvider); + + /// + /// Verifies that the subject is parsable into type . + /// + /// + /// The optional parameter provides culture-specific formatting information + /// in the call to . + /// + public static IsUtf8SpanParsableResult IsParsableInto( + this IThat> source, + IFormatProvider? formatProvider = null) + where TType : IUtf8SpanParsable + => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsUtf8ParsableIntoConstraint(it, grammars, formatProvider)), + source, + formatProvider); + + /// + /// Verifies that the subject is not parsable into type . + /// + /// + /// The optional parameter provides culture-specific formatting information + /// in the call to . + /// + public static AndOrResult, IThat>> IsNotParsableInto( + this IThat> source, + IFormatProvider? formatProvider = null) + where TType : ISpanParsable + => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsParsableIntoConstraint(it, grammars, formatProvider).Invert()), + source); + + /// + /// Verifies that the subject is not parsable into type . + /// + /// + /// The optional parameter provides culture-specific formatting information + /// in the call to . + /// + public static AndOrResult, IThat>> IsNotParsableInto( + this IThat> source, + IFormatProvider? formatProvider = null) + where TType : IUtf8SpanParsable + => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsUtf8ParsableIntoConstraint(it, grammars, formatProvider).Invert()), + source); + + private sealed class IsParsableIntoConstraint : ConstraintResult.WithValue>, + IValueConstraint> + where TType : ISpanParsable + { + private readonly IFormatProvider? _formatProvider; + private readonly string _it; + private string? _exceptionMessage; + + public IsParsableIntoConstraint(string it, + ExpectationGrammars grammars, + IFormatProvider? formatProvider) : base(grammars) + { + _it = it; + _formatProvider = formatProvider; + FurtherProcessingStrategy = FurtherProcessingStrategy.IgnoreResult; + } + + public ConstraintResult IsMetBy(SpanWrapper actual) + { + Actual = actual; + + try + { + _ = TType.Parse(actual.AsSpan(), _formatProvider); + Outcome = Outcome.Success; + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(ex.Message) || ex.Message.Length < 2) + { + _exceptionMessage = "an unknown error occurred"; + } + else + { + _exceptionMessage = char.ToLowerInvariant(ex.Message[0]) + ex.Message[1..^1]; + } + + Outcome = Outcome.Failure; + } + + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append("is parsable into "); + Formatter.Format(stringBuilder, typeof(TType)); + if (_formatProvider is not null) + { + stringBuilder.Append(" using "); + Formatter.Format(stringBuilder, _formatProvider); + } + } + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append(_it).Append(" was not because ").Append(_exceptionMessage); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append("is not parsable into "); + Formatter.Format(stringBuilder, typeof(TType)); + if (_formatProvider is not null) + { + stringBuilder.Append(" using "); + Formatter.Format(stringBuilder, _formatProvider); + } + } + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append(_it).Append(" was"); + } + + private sealed class IsUtf8ParsableIntoConstraint : ConstraintResult.WithValue>, + IValueConstraint> + where TType : IUtf8SpanParsable + { + private readonly IFormatProvider? _formatProvider; + private readonly string _it; + private string? _exceptionMessage; + + public IsUtf8ParsableIntoConstraint(string it, + ExpectationGrammars grammars, + IFormatProvider? formatProvider) : base(grammars) + { + _it = it; + _formatProvider = formatProvider; + FurtherProcessingStrategy = FurtherProcessingStrategy.IgnoreResult; + } + + public ConstraintResult IsMetBy(SpanWrapper actual) + { + Actual = actual; + + try + { + _ = TType.Parse(actual.AsSpan(), _formatProvider); + Outcome = Outcome.Success; + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(ex.Message) || ex.Message.Length < 2) + { + _exceptionMessage = "an unknown error occurred"; + } + else + { + _exceptionMessage = char.ToLowerInvariant(ex.Message[0]) + ex.Message[1..^1]; + } + + Outcome = Outcome.Failure; + } + + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append("is parsable into "); + Formatter.Format(stringBuilder, typeof(TType)); + if (_formatProvider is not null) + { + stringBuilder.Append(" using "); + Formatter.Format(stringBuilder, _formatProvider); + } + } + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append(_it).Append(" was not because ").Append(_exceptionMessage); + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append("is not parsable into "); + Formatter.Format(stringBuilder, typeof(TType)); + if (_formatProvider is not null) + { + stringBuilder.Append(" using "); + Formatter.Format(stringBuilder, _formatProvider); + } + } + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append(_it).Append(" was"); + } +} +#endif diff --git a/Source/aweXpect/That/Spans/ThatSpan.cs b/Source/aweXpect/That/Spans/ThatSpan.cs new file mode 100644 index 000000000..d3a135ce7 --- /dev/null +++ b/Source/aweXpect/That/Spans/ThatSpan.cs @@ -0,0 +1,10 @@ +#if NET8_0_OR_GREATER +using aweXpect.Core; + +namespace aweXpect; + +/// +/// Expectations on values. +/// +public static partial class ThatSpan; +#endif diff --git a/Source/aweXpect/aweXpect.csproj.DotSettings b/Source/aweXpect/aweXpect.csproj.DotSettings index 13cb71e74..138aaf1ff 100644 --- a/Source/aweXpect/aweXpect.csproj.DotSettings +++ b/Source/aweXpect/aweXpect.csproj.DotSettings @@ -17,6 +17,7 @@ True True True + True True True True diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt index 96d6dfc55..acb945c1d 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt @@ -1088,6 +1088,17 @@ namespace aweXpect public static aweXpect.Results.SignalCountWhoseResult Signaled(this aweXpect.Core.IThat> source) { } public static aweXpect.Results.SignalCountWhoseResult Signaled(this aweXpect.Core.IThat> source, aweXpect.Core.Times times) { } } + public static class ThatSpan + { + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> IsNotParsableInto(this aweXpect.Core.IThat> source, System.IFormatProvider? formatProvider = null) + where TType : System.IUtf8SpanParsable { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> IsNotParsableInto(this aweXpect.Core.IThat> source, System.IFormatProvider? formatProvider = null) + where TType : System.ISpanParsable { } + public static aweXpect.Results.IsUtf8SpanParsableResult IsParsableInto(this aweXpect.Core.IThat> source, System.IFormatProvider? formatProvider = null) + where TType : System.IUtf8SpanParsable { } + public static aweXpect.Results.IsSpanParsableResult IsParsableInto(this aweXpect.Core.IThat> source, System.IFormatProvider? formatProvider = null) + where TType : System.ISpanParsable { } + } public static class ThatStream { public static aweXpect.Results.PropertyResult.Long HasLength(this aweXpect.Core.IThat source) { } @@ -1268,6 +1279,18 @@ namespace aweXpect.Results public IsParsableResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat subject, System.IFormatProvider? formatProvider) { } public aweXpect.Core.IThat Which { get; } } + public class IsSpanParsableResult : aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> + where TType : System.ISpanParsable + { + public IsSpanParsableResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat> subject, System.IFormatProvider? formatProvider) { } + public aweXpect.Core.IThat Which { get; } + } + public class IsUtf8SpanParsableResult : aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> + where TType : System.IUtf8SpanParsable + { + public IsUtf8SpanParsableResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat> subject, System.IFormatProvider? formatProvider) { } + public aweXpect.Core.IThat Which { get; } + } public class ObjectCollectionBeContainedInResult : aweXpect.Results.ObjectCollectionMatchResult { public ObjectCollectionBeContainedInResult(aweXpect.Core.ExpectationBuilder expectationBuilder, TThat returnValue, aweXpect.Options.ObjectEqualityOptions options, aweXpect.Options.CollectionMatchOptions collectionMatchOptions) { } diff --git a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt index ed3cf455f..c86db747d 100644 --- a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt +++ b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt @@ -272,6 +272,20 @@ namespace aweXpect.Core public aweXpect.Core.ResultContexts Remove(System.Predicate predicate) { } public aweXpect.Core.ResultContexts Remove(string title, System.StringComparison stringComparison = 5) { } } + public class SpanWrapper : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.IEnumerable + { + public SpanWrapper(System.ReadOnlySpan span) { } + public SpanWrapper(System.Span span) { } + public int Count { get; } + public bool IsReadOnly { get; } + public void Add(T item) { } + public System.Span AsSpan() { } + public void Clear() { } + public bool Contains(T item) { } + public void CopyTo(T[] array, int arrayIndex) { } + public System.Collections.Generic.IEnumerator GetEnumerator() { } + public bool Remove(T item) { } + } public sealed class StringDifference { public StringDifference(string? actual, string? expected, System.Collections.Generic.IEqualityComparer? comparer = null, aweXpect.Core.StringDifferenceSettings? settings = null) { } @@ -585,6 +599,8 @@ namespace aweXpect public static aweXpect.Delegates.ThatDelegate.WithValue That(System.Func> @delegate, [System.Runtime.CompilerServices.CallerArgumentExpression("delegate")] string doNotPopulateThisValue = "") { } public static aweXpect.Delegates.ThatDelegate.WithValue That(System.Func> @delegate, [System.Runtime.CompilerServices.CallerArgumentExpression("delegate")] string doNotPopulateThisValue = "") { } public static aweXpect.Delegates.ThatDelegate.WithValue That(System.Func @delegate, [System.Runtime.CompilerServices.CallerArgumentExpression("delegate")] string doNotPopulateThisValue = "") { } + public static aweXpect.Core.IThat> That(System.ReadOnlySpan subject, [System.Runtime.CompilerServices.CallerArgumentExpression("subject")] string doNotPopulateThisValue = "") { } + public static aweXpect.Core.IThat> That(System.Span subject, [System.Runtime.CompilerServices.CallerArgumentExpression("subject")] string doNotPopulateThisValue = "") { } public static aweXpect.Core.IThat That(System.Threading.Tasks.Task subject, [System.Runtime.CompilerServices.CallerArgumentExpression("subject")] string doNotPopulateThisValue = "") { } public static aweXpect.Core.IThat That(System.Threading.Tasks.ValueTask subject, [System.Runtime.CompilerServices.CallerArgumentExpression("subject")] string doNotPopulateThisValue = "") { } public static aweXpect.Core.IThat That(T subject, [System.Runtime.CompilerServices.CallerArgumentExpression("subject")] string doNotPopulateThisValue = "") { } diff --git a/Tests/aweXpect.Core.Tests/Core/SpanWrapperTests.cs b/Tests/aweXpect.Core.Tests/Core/SpanWrapperTests.cs new file mode 100644 index 000000000..610ecaacb --- /dev/null +++ b/Tests/aweXpect.Core.Tests/Core/SpanWrapperTests.cs @@ -0,0 +1,89 @@ +#if NET8_0_OR_GREATER +using aweXpect.Synchronous; + +// ReSharper disable CollectionNeverUpdated.Local +// ReSharper disable CollectionNeverQueried.Local + +namespace aweXpect.Core.Tests.Core; + +public sealed class SpanWrapperTests +{ + [Fact] + public void Add_ShouldThrowNotSupportedException() + { + SpanWrapper sut = new("foo".AsSpan()); + + void Act() + => sut.Add('b'); + + Synchronously.Verify(That(Act).Throws() + .WithMessage("You may not change a SpanWrapper!")); + } + + [Fact] + public void Clear_ShouldThrowNotSupportedException() + { + SpanWrapper sut = new("foo".AsSpan()); + + void Act() + => sut.Clear(); + + Synchronously.Verify(That(Act).Throws() + .WithMessage("You may not change a SpanWrapper!")); + } + + [Theory] + [InlineData('o', true)] + [InlineData('x', false)] + public void Contains_ShouldReturnExpectedResult(char character, bool expectedResult) + { + SpanWrapper sut = new("foo".AsSpan()); + + bool result = sut.Contains(character); + + Synchronously.Verify(That(result).IsEqualTo(expectedResult)); + } + + [Fact] + public void CopyTo_ShouldCopyValuesToArray() + { + char[] buffer = "some-prefilled-buffer".ToCharArray(); + SpanWrapper sut = new("foo".AsSpan()); + + sut.CopyTo(buffer, 1); + + Synchronously.Verify(That(new string(buffer)).IsEqualTo("sfoo-prefilled-buffer")); + } + + [Theory] + [InlineData("foo", 3)] + [InlineData("foobar", 6)] + public void Count_ShouldReturnExpectedLength(string subject, int expectedLength) + { + SpanWrapper sut = new(subject.AsSpan()); + + Synchronously.Verify(That(sut.Count).IsEqualTo(expectedLength)); + } + + [Fact] + public void IsReadOnly_ShouldBeTrue() + { + Span span = [1, 2, 3,]; + SpanWrapper sut = new(span); + + Synchronously.Verify(That(sut.IsReadOnly).IsTrue()); + } + + [Fact] + public void Remove_ShouldThrowNotSupportedException() + { + SpanWrapper sut = new("foo".AsSpan()); + + void Act() + => sut.Remove('b'); + + Synchronously.Verify(That(Act).Throws() + .WithMessage("You may not change a SpanWrapper!")); + } +} +#endif diff --git a/Tests/aweXpect.Core.Tests/ExpectTests.cs b/Tests/aweXpect.Core.Tests/ExpectTests.cs index 4027d58ba..2b97fcf11 100644 --- a/Tests/aweXpect.Core.Tests/ExpectTests.cs +++ b/Tests/aweXpect.Core.Tests/ExpectTests.cs @@ -115,6 +115,7 @@ private sealed class MyExpectation(Expectation.Result result, params ResultConte { internal override Task GetResult(int index, Dictionary outcomes) => Task.FromResult(result); + internal override IEnumerable GetContexts(int index, Dictionary outcomes) => contexts; } diff --git a/Tests/aweXpect.Tests/Spans/ThatSpan.IsNotParsableInto.Tests.cs b/Tests/aweXpect.Tests/Spans/ThatSpan.IsNotParsableInto.Tests.cs new file mode 100644 index 000000000..c79df8349 --- /dev/null +++ b/Tests/aweXpect.Tests/Spans/ThatSpan.IsNotParsableInto.Tests.cs @@ -0,0 +1,55 @@ +#if NET8_0_OR_GREATER +using System.Globalization; + +namespace aweXpect.Tests; + +public sealed partial class ThatSpan +{ + public sealed partial class IsNotParsableInto + { + public sealed class Tests + { + [Fact] + public async Task WhenSpanIsNotParsable_ShouldSucceed() + { + async Task Act() + => await That("abc".AsSpan()).IsNotParsableInto(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSpanIsParsable_ShouldFail() + { + async Task Act() + => await That("42".AsSpan()).IsNotParsableInto(); + + await That(Act).Throws() + .WithMessage(""" + Expected that "42".AsSpan() + is not parsable into int, + but it was + """); + } + + [Theory] + [InlineData("12,34", "de-AT")] + [InlineData("12.34", "en-US")] + public async Task WithFormatProvider_ShouldBeUsed(string subject, string cultureName) + { + IFormatProvider formatProvider = new CultureInfo(cultureName); + + async Task Act() + => await That(subject.AsSpan()).IsNotParsableInto(formatProvider); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject.AsSpan() + is not parsable into decimal using {cultureName}, + but it was + """); + } + } + } +} +#endif diff --git a/Tests/aweXpect.Tests/Spans/ThatSpan.IsNotParsableInto.Utf8Tests.cs b/Tests/aweXpect.Tests/Spans/ThatSpan.IsNotParsableInto.Utf8Tests.cs new file mode 100644 index 000000000..b9d780bd3 --- /dev/null +++ b/Tests/aweXpect.Tests/Spans/ThatSpan.IsNotParsableInto.Utf8Tests.cs @@ -0,0 +1,61 @@ +#if NET8_0_OR_GREATER +using System.Globalization; +using System.Text; + +namespace aweXpect.Tests; + +public sealed partial class ThatSpan +{ + public sealed partial class IsNotParsableInto + { + public sealed class Utf8Tests + { + [Fact] + public async Task WhenSpanIsNotParsable_ShouldSucceed() + { + byte[] subject = "abc"u8.ToArray(); + + async Task Act() + => await That(subject.AsSpan()).IsNotParsableInto(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSpanIsParsable_ShouldFail() + { + byte[] subject = "42"u8.ToArray(); + + async Task Act() + => await That(subject.AsSpan()).IsNotParsableInto(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject.AsSpan() + is not parsable into int, + but it was + """); + } + + [Theory] + [InlineData("12,34", "de-AT")] + [InlineData("12.34", "en-US")] + public async Task WithFormatProvider_ShouldBeUsed(string subjectString, string cultureName) + { + byte[] subject = Encoding.UTF8.GetBytes(subjectString); + IFormatProvider formatProvider = new CultureInfo(cultureName); + + async Task Act() + => await That(subject.AsSpan()).IsNotParsableInto(formatProvider); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject.AsSpan() + is not parsable into decimal using {cultureName}, + but it was + """); + } + } + } +} +#endif diff --git a/Tests/aweXpect.Tests/Spans/ThatSpan.IsParsableInto.Tests.cs b/Tests/aweXpect.Tests/Spans/ThatSpan.IsParsableInto.Tests.cs new file mode 100644 index 000000000..5b86da4a3 --- /dev/null +++ b/Tests/aweXpect.Tests/Spans/ThatSpan.IsParsableInto.Tests.cs @@ -0,0 +1,107 @@ +#if NET8_0_OR_GREATER +using System.Globalization; + +namespace aweXpect.Tests; + +public sealed partial class ThatSpan +{ + public sealed partial class IsParsableInto + { + public sealed class Tests + { + [Fact] + public async Task WhenSpanIsNotParsable_ShouldFail() + { + async Task Act() + => await That("abc".AsSpan()).IsParsableInto(); + + await That(Act).Throws() + .WithMessage(""" + Expected that "abc".AsSpan() + is parsable into int, + but it was not because the input string 'abc' was not in a correct format + """); + } + + [Fact] + public async Task WhenSpanIsParsable_ShouldSucceed() + { + async Task Act() + => await That("42".AsSpan()).IsParsableInto(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData("-12.34", "de-AT")] + [InlineData("-12,34", "en-US")] + public async Task WithFormatProvider_WhenFormatDoesNotMatch_ShouldFail(string subject, string cultureName) + { + IFormatProvider formatProvider = new CultureInfo(cultureName); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto(formatProvider); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject.AsSpan() + is parsable into uint using {cultureName}, + but it was not because the input string '{subject}' was not in a correct format + """); + } + + [Theory] + [InlineData("12,34", "de-AT")] + [InlineData("12.34", "en-US")] + public async Task WithFormatProvider_WhenFormatMatches_ShouldSucceed(string subject, string cultureName) + { + IFormatProvider formatProvider = new CultureInfo(cultureName); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto(formatProvider); + + await That(Act).DoesNotThrow(); + } + } + + public sealed class WhichTests + { + [Fact] + public async Task WhenSpanIsNotParsable_ShouldFail() + { + async Task Act() + => await That("abc".AsSpan()).IsParsableInto().Which.IsLessThan(10.Seconds()); + + await That(Act).Throws() + .WithMessage(""" + Expected that "abc".AsSpan() + is parsable into TimeSpan which is less than 0:10, + but it was not because string 'abc' was not recognized as a valid TimeSpan + """); + } + + [Fact] + public async Task WhenSpanIsParsable_ShouldSucceed() + { + async Task Act() + => await That("42".AsSpan()).IsParsableInto().Which.IsBetween(41).And(43); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData("12,34", "de-AT")] + [InlineData("12.34", "en-US")] + public async Task WithFormatProvider_ShouldBeUsed(string subject, string cultureName) + { + IFormatProvider formatProvider = new CultureInfo(cultureName); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto(formatProvider).Which.IsEqualTo(12.34M); + + await That(Act).DoesNotThrow(); + } + } + } +} +#endif diff --git a/Tests/aweXpect.Tests/Spans/ThatSpan.IsParsableInto.Utf8Tests.cs b/Tests/aweXpect.Tests/Spans/ThatSpan.IsParsableInto.Utf8Tests.cs new file mode 100644 index 000000000..a2ff3c8d4 --- /dev/null +++ b/Tests/aweXpect.Tests/Spans/ThatSpan.IsParsableInto.Utf8Tests.cs @@ -0,0 +1,118 @@ +#if NET8_0_OR_GREATER +using System.Globalization; +using System.Text; + +namespace aweXpect.Tests; + +public sealed partial class ThatSpan +{ + public sealed partial class IsParsableInto + { + public sealed class Utf8Tests + { + [Fact] + public async Task WhenSpanIsNotParsable_ShouldFail() + { + byte[] subject = "abc"u8.ToArray(); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject.AsSpan() + is parsable into int, + but it was not because the input string 'System.ReadOnlySpan[3]' was not in a correct format + """); + } + + [Fact] + public async Task WhenSpanIsParsable_ShouldSucceed() + { + byte[] subject = "42"u8.ToArray(); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData("-12.34", "de-AT")] + [InlineData("-12,34", "en-US")] + public async Task WithFormatProvider_WhenFormatDoesNotMatch_ShouldFail(string subjectString, + string cultureName) + { + byte[] subject = Encoding.UTF8.GetBytes(subjectString); + IFormatProvider formatProvider = new CultureInfo(cultureName); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto(formatProvider); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject.AsSpan() + is parsable into uint using {cultureName}, + but it was not because the input string 'System.ReadOnlySpan[6]' was not in a correct format + """); + } + + [Theory] + [InlineData("12,34", "de-AT")] + [InlineData("12.34", "en-US")] + public async Task WithFormatProvider_WhenFormatMatches_ShouldSucceed(string subjectString, + string cultureName) + { + byte[] subject = Encoding.UTF8.GetBytes(subjectString); + IFormatProvider formatProvider = new CultureInfo(cultureName); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto(formatProvider); + + await That(Act).DoesNotThrow(); + } + } + + public sealed class Utf8WhichTests + { + [Fact] + public async Task WhenSpanIsNotParsable_ShouldFail() + { + byte[] subject = "abc"u8.ToArray(); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto().Which.IsLessThan(10.0); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject.AsSpan() + is parsable into double which is less than 10.0, + but it was not because the input string 'System.ReadOnlySpan[3]' was not in a correct format + """); + } + + [Fact] + public async Task WhenSpanIsParsable_ShouldSucceed() + { + async Task Act() + => await That("42".AsSpan()).IsParsableInto().Which.IsBetween(41).And(43); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData("12,34", "de-AT")] + [InlineData("12.34", "en-US")] + public async Task WithFormatProvider_ShouldBeUsed(string subject, string cultureName) + { + IFormatProvider formatProvider = new CultureInfo(cultureName); + + async Task Act() + => await That(subject.AsSpan()).IsParsableInto(formatProvider).Which.IsEqualTo(12.34M); + + await That(Act).DoesNotThrow(); + } + } + } +} +#endif diff --git a/Tests/aweXpect.Tests/Spans/ThatSpan.Tests.cs b/Tests/aweXpect.Tests/Spans/ThatSpan.Tests.cs new file mode 100644 index 000000000..badba5add --- /dev/null +++ b/Tests/aweXpect.Tests/Spans/ThatSpan.Tests.cs @@ -0,0 +1,43 @@ +#if NET8_0_OR_GREATER +namespace aweXpect.Tests; + +public sealed partial class ThatSpan +{ + public sealed class Tests + { + [Fact] + public async Task ShouldSupportIsEmpty() + { + var subject = new[] + { + 1, 2, 3, + }; + async Task Act() + => await That(subject.AsSpan()).IsEmpty(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject.AsSpan() + is empty, + but it was [ + 1, + 2, + 3 + ] + """); + } + + [Fact] + public async Task ShouldSupportIsInAscendingOrder() + { + async Task Act() + => await That(new[] + { + 1, 2, 3, + }.AsSpan()).IsInAscendingOrder(); + + await That(Act).DoesNotThrow(); + } + } +} +#endif diff --git a/Tests/aweXpect.Tests/Spans/ThatSpan.cs b/Tests/aweXpect.Tests/Spans/ThatSpan.cs new file mode 100644 index 000000000..57c0aae99 --- /dev/null +++ b/Tests/aweXpect.Tests/Spans/ThatSpan.cs @@ -0,0 +1,7 @@ +#if NET8_0_OR_GREATER +namespace aweXpect.Tests; + +public sealed partial class ThatSpan +{ +} +#endif diff --git a/Tests/aweXpect.Tests/aweXpect.Tests.csproj.DotSettings b/Tests/aweXpect.Tests/aweXpect.Tests.csproj.DotSettings index 6de2cb12f..73ecc708b 100644 --- a/Tests/aweXpect.Tests/aweXpect.Tests.csproj.DotSettings +++ b/Tests/aweXpect.Tests/aweXpect.Tests.csproj.DotSettings @@ -15,6 +15,7 @@ True True True + True True True True