diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a62f389..05938315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,25 @@ All notable changes to this project will be documented in this file. - Extensions: - ConfirmIsOdd - ConfirmIsEven + - ConfirmCloseTo - Extension classes: - RandomEnumExtensions + - RandomBooleanExtensions + - RandomNetworkExtensions - Confirm class with assertions: - IsEnumValue - IsNotEnumValue - IsEnumName - IsNotEnumName + - IsTrue + - IsFalse + - Throws + - NotThrows + +### Changed + +- Numeric extensions now use 'INumber' generic constraint + instead of 'IComparable, IConvertible, IComparable, IEquatable'. ## [0.5.0-beta 2024-06-29] diff --git a/addons/confirma/src/classes/Confirm.cs b/addons/confirma/src/classes/Confirm.cs index fdd1f0b5..021c2562 100644 --- a/addons/confirma/src/classes/Confirm.cs +++ b/addons/confirma/src/classes/Confirm.cs @@ -1,10 +1,25 @@ using System; using Confirma.Exceptions; +using Confirma.Extensions; namespace Confirma.Classes; public static class Confirm { + public static bool IsTrue(bool expression, string? message = null) + { + if (expression) return true; + + throw new ConfirmAssertException(message ?? "Expected true but was false"); + } + + public static bool IsFalse(bool expression, string? message = null) + { + if (!expression) return true; + + throw new ConfirmAssertException(message ?? "Expected false but was true"); + } + #region IsEnumValue public static int IsEnumValue(int value, string? message = null) where T : struct, Enum @@ -77,4 +92,18 @@ public static string IsNotEnumName(string name, string? message = null) ); } #endregion + + #region Throws + public static Action Throws(Action action, string? message = null) + where T : Exception + { + return action.ConfirmThrows(message); + } + + public static Action NotThrows(Action action, string? message = null) + where T : Exception + { + return action.ConfirmNotThrows(message); + } + #endregion } diff --git a/addons/confirma/src/extensions/ConfirmNumericExtensions.cs b/addons/confirma/src/extensions/ConfirmNumericExtensions.cs index 00a2b90c..73fee456 100644 --- a/addons/confirma/src/extensions/ConfirmNumericExtensions.cs +++ b/addons/confirma/src/extensions/ConfirmNumericExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using Confirma.Exceptions; namespace Confirma.Extensions; @@ -10,7 +11,7 @@ public static class ConfirmNumericExtensions /// Zero is not considered positive. /// public static T ConfirmIsPositive(this T actual, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if (actual.CompareTo((T)Convert.ChangeType(0, typeof(T))) > 0) return actual; @@ -21,7 +22,7 @@ public static T ConfirmIsPositive(this T actual, string? message = null) } public static T ConfirmIsNotPositive(this T actual, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if (actual.CompareTo((T)Convert.ChangeType(0, typeof(T))) <= 0) return actual; @@ -37,7 +38,7 @@ public static T ConfirmIsNotPositive(this T actual, string? message = null) /// Zero is not considered negative. /// public static T ConfirmIsNegative(this T actual, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if (actual.CompareTo((T)Convert.ChangeType(0, typeof(T))) < 0) return actual; @@ -48,7 +49,7 @@ public static T ConfirmIsNegative(this T actual, string? message = null) } public static T ConfirmIsNotNegative(this T actual, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if (actual.CompareTo((T)Convert.ChangeType(0, typeof(T))) >= 0) return actual; @@ -64,7 +65,7 @@ public static T ConfirmIsNotNegative(this T actual, string? message = null) /// Zero is not considered signed or unsigned. /// public static T ConfirmSign(this T actual, bool sign, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if (sign && actual.CompareTo((T)Convert.ChangeType(0, typeof(T))) < 0) return actual; if (!sign && actual.CompareTo((T)Convert.ChangeType(0, typeof(T))) > 0) return actual; @@ -78,7 +79,7 @@ public static T ConfirmSign(this T actual, bool sign, string? message = null) #region ConfirmIsZero public static T ConfirmIsZero(this T actual, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if (actual.CompareTo((T)Convert.ChangeType(0, typeof(T))) == 0) return actual; @@ -89,7 +90,7 @@ public static T ConfirmIsZero(this T actual, string? message = null) } public static T ConfirmIsNotZero(this T actual, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if (actual.CompareTo((T)Convert.ChangeType(0, typeof(T))) != 0) return actual; @@ -101,7 +102,7 @@ public static T ConfirmIsNotZero(this T actual, string? message = null) #endregion public static T ConfirmIsOdd(this T actual, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if ((Convert.ToInt64(actual) & 1) != 0) return actual; @@ -109,10 +110,29 @@ public static T ConfirmIsOdd(this T actual, string? message = null) } public static T ConfirmIsEven(this T actual, string? message = null) - where T : IComparable, IConvertible, IComparable, IEquatable + where T : INumber { if ((Convert.ToInt64(actual) & 1) == 0) return actual; throw new ConfirmAssertException(message ?? $"Expected {actual} to be even."); } + + public static T ConfirmCloseTo( + this T actual, + T expected, + T tolerance, + string? message = null + ) + where T : INumber + { + T diff = actual - expected; + T abs = diff < (T)Convert.ChangeType(0, typeof(T)) ? -diff : diff; + + if (abs <= tolerance) return actual; + + throw new ConfirmAssertException( + message ?? + $"{actual} is not close to {expected} within tolerance {tolerance}." + ); + } } diff --git a/addons/confirma/src/extensions/RandomBooleanExtensions.cs b/addons/confirma/src/extensions/RandomBooleanExtensions.cs new file mode 100644 index 00000000..7f2b3778 --- /dev/null +++ b/addons/confirma/src/extensions/RandomBooleanExtensions.cs @@ -0,0 +1,21 @@ +using System; + +namespace Confirma.Extensions; + +public static class RandomBooleanExtensions +{ + public static bool NextBool(this Random rg) + { + return rg.Next(0, 2) == 1; + } + + public static bool? NextNullableBool(this Random rg) + { + return rg.Next(0, 3) switch + { + 0 => false, + 1 => true, + _ => null, + }; + } +} diff --git a/addons/confirma/src/extensions/RandomNetworkExtensions.cs b/addons/confirma/src/extensions/RandomNetworkExtensions.cs new file mode 100644 index 00000000..a0d2c1b4 --- /dev/null +++ b/addons/confirma/src/extensions/RandomNetworkExtensions.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace Confirma.Extensions; + +public static class RandomNetworkExtensions +{ + private readonly static List _domains = new() { + "gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "proton.me" + }; + + public static IPAddress NextIPAddress(this Random rg) + { + var data = new byte[4]; + rg.NextBytes(data); + + data[0] |= 1; + + return new(data); + } + + public static IPAddress NextIP6Address(this Random rg) + { + var data = new byte[16]; + rg.NextBytes(data); + + data[0] |= 1; + + return new(data); + } + + public static string NextEmail(this Random rg, int minLength = 8, int maxLength = 12) + { + if (minLength < 1 || maxLength < minLength) + throw new ArgumentException("Invalid length parameters"); + + int length = rg.Next(minLength, maxLength + 1); + var localPart = new StringBuilder(length); + + for (int i = 0; i < length; i++) + { + if (i < length - 1 && rg.Next(6) == 0) + localPart.Append(rg.Next(2) == 0 ? '-' : '_'); + else + { + if (rg.Next(2) == 0) + localPart.Append((char)('a' + rg.Next(26))); + else + localPart.Append(rg.Next(10)); + } + } + + var domain = _domains.ElementAt(rg.Next(_domains.Count)); + return $"{localPart}@{domain}"; + } +} diff --git a/tests/ConfirmNumericTest.cs b/tests/ConfirmNumericTest.cs index 27091f3f..2c59b7cc 100644 --- a/tests/ConfirmNumericTest.cs +++ b/tests/ConfirmNumericTest.cs @@ -155,4 +155,34 @@ public static void ConfirmIsEven_WhenIsOdd(int actual) action.ConfirmThrows(); } #endregion + + #region ConfirmCloseTo + [TestCase(5f, 6f, 2f)] + [TestCase(5f, 5.1f, 0.1f)] + [TestCase(6f, 5f, 2f)] + [TestCase(5.1f, 5f, 0.1f)] + [TestCase(-5f, -4.5f, 0.5f)] + public static void ConfirmCloseTo_WhenCloseTo( + float actual, + float expected, + float tolerance + ) + { + actual.ConfirmCloseTo(expected, tolerance); + } + + [TestCase(5d, 0d, 1d)] + [TestCase(5d, 15d, 1d)] + [TestCase(0d, 0.1d, 0.01d)] + public static void ConfirmCloseTo_WhenNotCloseTo( + double actual, + double expected, + double tolerance + ) + { + Action action = () => actual.ConfirmCloseTo(expected, tolerance); + + action.ConfirmThrows(); + } + #endregion } diff --git a/tests/ConfirmTest.cs b/tests/ConfirmTest.cs index 0cb7f8a9..c1006f13 100644 --- a/tests/ConfirmTest.cs +++ b/tests/ConfirmTest.cs @@ -94,4 +94,35 @@ public static void IsEnumName_WhenIsNotEnumName(string name, bool ignoreCase) action.ConfirmThrows(); } #endregion + + #region IsTrue + [TestCase] + public static void IsTrue_WhenIsTrue() + { + Confirm.IsTrue(true); + } + + [TestCase] + public static void IsTrue_WhenIsFalse() + { + Action action = () => Confirm.IsTrue(false); + + action.ConfirmThrows(); + } + #endregion + + #region IsFalse + public static void IsFalse_WhenIsFalse() + { + Confirm.IsFalse(false); + } + + [TestCase] + public static void IsFalse_WhenIsTrue() + { + Action action = () => Confirm.IsFalse(true); + + action.ConfirmThrows(); + } + #endregion } diff --git a/tests/RandomBooleanTest.cs b/tests/RandomBooleanTest.cs new file mode 100644 index 00000000..7ee2af0a --- /dev/null +++ b/tests/RandomBooleanTest.cs @@ -0,0 +1,53 @@ +using System; +using Confirma.Attributes; +using Confirma.Extensions; + +namespace Confirma.Tests; + +[TestClass] +[Parallelizable] +public static class RandomBooleanTest +{ + private readonly static Random rg = new(); + + [Repeat(5)] + [TestCase] + public static void NextBool() + { + const uint ITERATIONS = 100000; + uint trueCount = 0; + + for (int i = 0; i < ITERATIONS; i++) if (rg.NextBool()) trueCount++; + + var truePercentage = (double)trueCount / ITERATIONS * 100; + truePercentage.ConfirmCloseTo(50, 1); + } + + [Repeat(5)] + [TestCase] + public static void NextNullableBool() + { + const uint ITERATIONS = 100000; + + uint trueCount = 0; + uint falseCount = 0; + uint nullCount = 0; + + for (int i = 0; i < ITERATIONS; i++) + { + bool? result = rg.NextNullableBool(); + + if (result == true) trueCount++; + else if (result == false) falseCount++; + else nullCount++; + } + + var truePercentage = (double)trueCount / ITERATIONS * 100; + var falsePercentage = (double)falseCount / ITERATIONS * 100; + var nullPercentage = (double)nullCount / ITERATIONS * 100; + + nullPercentage.ConfirmCloseTo(33.33, 1); + truePercentage.ConfirmCloseTo(33.33, 1); + falsePercentage.ConfirmCloseTo(33.33, 1); + } +} diff --git a/tests/RandomNetworkTest.cs b/tests/RandomNetworkTest.cs new file mode 100644 index 00000000..b91ad280 --- /dev/null +++ b/tests/RandomNetworkTest.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using System.Net.Sockets; +using Confirma.Attributes; +using Confirma.Classes; +using Confirma.Extensions; + +namespace Confirma.Tests; + +[TestClass] +[Parallelizable] +public static class RandomNetworkTest +{ + private readonly static Random _rg = new(); + + #region NextIPAddress + [Repeat(5)] + [TestCase] + public static void NextIPAddress_GeneratesValidIPv4Address() + { + var ipAddress = _rg.NextIPAddress(); + + Confirm.IsTrue(ipAddress.AddressFamily == AddressFamily.InterNetwork); + Confirm.IsTrue(ipAddress.GetAddressBytes().Length == 4); + } + + [Repeat(5)] + [TestCase] + public static void NextIPAddress_FirstOctetIsAlwaysOdd() + { + var ipAddress = _rg.NextIPAddress(); + + Confirm.IsTrue((ipAddress.GetAddressBytes()[0] & 1) == 1); + } + #endregion + + #region NextIP6Address + [Repeat(5)] + [TestCase] + public static void NextIP6Address_GeneratesValidIPv6Address() + { + var ipAddress = _rg.NextIP6Address(); + + Confirm.IsTrue(ipAddress.AddressFamily == AddressFamily.InterNetworkV6); + Confirm.IsTrue(ipAddress.GetAddressBytes().Length == 16); + } + + [Repeat(5)] + [TestCase] + public static void NextIP6Address_FirstHextetIsAlwaysOdd() + { + var ipAddress = _rg.NextIP6Address(); + + Confirm.IsTrue((ipAddress.GetAddressBytes()[0] & 1) == 1); + } + #endregion + + #region NextEmail + [TestCase] + public static void NextEmail_InvalidLengthParameters() + { + Confirm.Throws(() => _rg.NextEmail(-1, 12)); + Confirm.Throws(() => _rg.NextEmail(12, 8)); + } + + [Repeat(5)] + [TestCase] + public static void NextEmail_ReturnsValidEmail() + { + var minLength = _rg.Next(1, 64); + var maxLength = _rg.Next(minLength, minLength + 64); + var email = _rg.NextEmail(minLength, maxLength); + + email.ConfirmNotNull(); + email.Contains('@').ConfirmTrue(); + + var parts = email.Split('@'); + Confirm.IsTrue(parts[0].Length >= minLength && parts[0].Length <= maxLength); + } + + [Repeat(5)] + [TestCase] + public static void NextEmail_LocalPartContainsValidCharacters() + { + var email = _rg.NextEmail(); + + var localPart = email.Split('@')[0]; + Confirm.IsTrue(localPart.All(c => char.IsLetterOrDigit(c) || c == '-' || c == '_')); + } + #endregion +}