From d23561261a8c98a9b1f9c46838fae3ecfca31998 Mon Sep 17 00:00:00 2001 From: Pent Ploompuu Date: Tue, 26 Sep 2017 01:36:29 +0300 Subject: [PATCH] BigInteger based random testing of System.Decimal (#24053) * BigInteger based random testing of Decimal * Comments for random testing of Decimal * Parallelize random testing of Decimal * Simplified BigDecimal.Div for Decimal tests --- .../tests/System/DecimalTests.cs | 385 ++++++++++++++++++ 1 file changed, 385 insertions(+) diff --git a/src/System.Runtime/tests/System/DecimalTests.cs b/src/System.Runtime/tests/System/DecimalTests.cs index 1c7738a8ac26..ce30b774fd24 100644 --- a/src/System.Runtime/tests/System/DecimalTests.cs +++ b/src/System.Runtime/tests/System/DecimalTests.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Linq; +using System.Numerics; using Xunit; namespace System.Tests @@ -1293,5 +1295,388 @@ public static void DecrementOperator(decimal d, decimal expected) { Assert.Equal(expected, --d); } + + public static class BigIntegerCompare + { + [Fact] + public static void Test() + { + decimal[] decimalValues = GetRandomData(out BigDecimal[] bigDecimals); + for (int i = 0; i < decimalValues.Length; i++) + { + decimal d1 = decimalValues[i]; + BigDecimal b1 = bigDecimals[i]; + for (int j = 0; j < decimalValues.Length; j++) + { + decimal d2 = decimalValues[j]; + int expected = b1.CompareTo(bigDecimals[j]); + int actual = d1.CompareTo(d2); + if (expected != actual) + throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, d1 + " CMP " + d2); + } + } + } + } + + public static class BigIntegerAdd + { + [Fact] + public static void Test() + { + int overflowBudget = 1000; + decimal[] decimalValues = GetRandomData(out BigDecimal[] bigDecimals); + for (int i = 0; i < decimalValues.Length; i++) + { + decimal d1 = decimalValues[i]; + BigDecimal b1 = bigDecimals[i]; + for (int j = 0; j < decimalValues.Length; j++) + { + decimal d2 = decimalValues[j]; + BigDecimal expected = b1.Add(bigDecimals[j], out bool expectedOverflow); + if (expectedOverflow) + { + if (--overflowBudget < 0) + continue; + try + { + decimal actual = d1 + d2; + throw new Xunit.Sdk.AssertActualExpectedException(typeof(OverflowException), actual, d1 + " + " + d2); + } + catch (OverflowException) { } + } + else + unsafe + { + decimal actual = d1 + d2; + if (expected.Scale != (byte)(*(uint*)&actual >> BigDecimal.ScaleShift) || expected.CompareTo(new BigDecimal(actual)) != 0) + throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, d1 + " + " + d2); + } + } + } + } + } + + public static class BigIntegerMul + { + [Fact] + public static void Test() + { + int overflowBudget = 1000; + decimal[] decimalValues = GetRandomData(out BigDecimal[] bigDecimals); + for (int i = 0; i < decimalValues.Length; i++) + { + decimal d1 = decimalValues[i]; + BigDecimal b1 = bigDecimals[i]; + for (int j = 0; j < decimalValues.Length; j++) + { + decimal d2 = decimalValues[j]; + BigDecimal expected = b1.Mul(bigDecimals[j], out bool expectedOverflow); + if (expectedOverflow) + { + if (--overflowBudget < 0) + continue; + try + { + decimal actual = d1 * d2; + throw new Xunit.Sdk.AssertActualExpectedException(typeof(OverflowException), actual, d1 + " * " + d2); + } + catch (OverflowException) { } + } + else + unsafe + { + decimal actual = d1 * d2; + if (expected.Scale != (byte)(*(uint*)&actual >> BigDecimal.ScaleShift) || expected.CompareTo(new BigDecimal(actual)) != 0) + throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, d1 + " * " + d2); + } + } + } + } + } + + public static class BigIntegerDiv + { + [Fact] + public static void Test() + { + int overflowBudget = 1000; + decimal[] decimalValues = GetRandomData(out BigDecimal[] bigDecimals); + for (int i = 0; i < decimalValues.Length; i++) + { + decimal d1 = decimalValues[i]; + BigDecimal b1 = bigDecimals[i]; + for (int j = 0; j < decimalValues.Length; j++) + { + decimal d2 = decimalValues[j]; + if (Math.Sign(d2) == 0) + continue; + BigDecimal expected = b1.Div(bigDecimals[j], out bool expectedOverflow); + if (expectedOverflow) + { + if (--overflowBudget < 0) + continue; + try + { + decimal actual = d1 / d2; + throw new Xunit.Sdk.AssertActualExpectedException(typeof(OverflowException), actual, d1 + " / " + d2); + } + catch (OverflowException) { } + } + else + unsafe + { + decimal actual = d1 / d2; + if (expected.Scale != (byte)(*(uint*)&actual >> BigDecimal.ScaleShift) || expected.CompareTo(new BigDecimal(actual)) != 0) + throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, d1 + " / " + d2); + } + } + } + } + } + + static decimal[] GetRandomData(out BigDecimal[] bigDecimals) + { + // some static data to test the limits + var list = new List { new decimal(0, 0, 0, true, 0), decimal.Zero, decimal.MinusOne, decimal.One, decimal.MinValue, decimal.MaxValue, + new decimal(1, 0, 0, true, 28), new decimal(1, 0, 0, false, 28), + new decimal(123877878, -16789245, 1086421879, true, 16), new decimal(527635459, -80701438, 1767087216, true, 24), new decimal(253511426, -909347550, -753557281, false, 12) }; + + // ~1000 different random decimals covering every scale and sign with ~20 different bitpatterns each + var rnd = new Random(42); + var unique = new HashSet(); + for (byte scale = 0; scale <= 28; scale++) + for (int sign = 0; sign <= 1; sign++) + for (int high = 0; high <= 96; high = IncBitLimits(high)) + for (int low = 0; low < high || (high | low) == 0; low = IncBitLimits(high)) + { + var d = new decimal(GetDigits(low, high), GetDigits(low - 32, high - 32), GetDigits(low - 64, high - 64), sign != 0, scale); + if (!unique.Add(d.ToString(CultureInfo.InvariantCulture))) + continue; // skip duplicates + list.Add(d); + } + decimal[] decimalValues = list.ToArray(); + bigDecimals = Array.ConvertAll(decimalValues, d => new BigDecimal(d)); + return decimalValues; + + // While the decimals are random in general, + // they are particularly focused on numbers starting or ending at bits 0-3, 30-34, 62-66, 94-96 to focus more on the corner cases around uint32 boundaries. + int IncBitLimits(int i) + { + switch (i) + { + case 3: + return 30; + case 34: + return 62; + case 66: + return 94; + default: + return i + 1; + } + } + + // Generates a random number, only bits between low and high can be set. + int GetDigits(int low, int high) + { + if (high <= 0 || low >= 32) + return 0; + uint res = 0; + if (high <= 32) + res = 1u << (high - 1); + res |= (uint)Math.Ceiling((uint.MaxValue >> Math.Max(0, 32 - high)) * rnd.NextDouble()); + if (low > 0) + res = (res >> low) << low; + return (int)res; + } + } + + /// + /// Decimal implementation roughly based on the oleaut32 native decimal code (especially ScaleResult), but optimized for simplicity instead of speed + /// + struct BigDecimal + { + public readonly BigInteger Integer; + public readonly byte Scale; + + public unsafe BigDecimal(decimal value) + { + Scale = (byte)(*(uint*)&value >> ScaleShift); + *(uint*)&value &= ~ScaleMask; + Integer = new BigInteger(value); + } + + private const uint ScaleMask = 0x00FF0000; + public const int ScaleShift = 16; + + public override string ToString() + { + if (Scale == 0) + return Integer.ToString(); + var s = Integer.ToString("D" + (Scale + 1)); + return s.Insert(s.Length - Scale, "."); + } + + BigDecimal(BigInteger integer, byte scale) + { + Integer = integer; + Scale = scale; + } + + static readonly BigInteger[] Pow10 = Enumerable.Range(0, 60).Select(i => BigInteger.Pow(10, i)).ToArray(); + + public int CompareTo(BigDecimal value) + { + int sd = Scale - value.Scale; + if (sd > 0) + return Integer.CompareTo(value.Integer * Pow10[sd]); + else if (sd < 0) + return (Integer * Pow10[-sd]).CompareTo(value.Integer); + else + return Integer.CompareTo(value.Integer); + } + + public BigDecimal Add(BigDecimal value, out bool overflow) + { + int sd = Scale - value.Scale; + BigInteger a = Integer, b = value.Integer; + if (sd > 0) + b *= Pow10[sd]; + else if (sd < 0) + a *= Pow10[-sd]; + + var res = a + b; + int scale = Math.Max(Scale, value.Scale); + overflow = ScaleResult(ref res, ref scale); + return new BigDecimal(res, (byte)scale); + } + + public BigDecimal Mul(BigDecimal value, out bool overflow) + { + var res = Integer * value.Integer; + int scale = Scale + value.Scale; + if (res.IsZero) + { + overflow = false; + // VarDecMul quirk: multipling by zero results in a scaled zero (e.g., 0.000) only if the intermediate scale is <=47 and both inputs fit in 32 bits! + if (scale <= 47 && BigInteger.Abs(Integer) <= MaxInteger32 && BigInteger.Abs(value.Integer) <= MaxInteger32) + scale = Math.Min(scale, 28); + else + scale = 0; + } + else + { + overflow = ScaleResult(ref res, ref scale); + // VarDecMul quirk: rounding to zero results in a scaled zero (e.g., 0.000), except if the intermediate scale is >47 and both inputs fit in 32 bits! + if (res.IsZero && scale == 28 && Scale + value.Scale > 47 && BigInteger.Abs(Integer) <= MaxInteger32 && BigInteger.Abs(value.Integer) <= MaxInteger32) + scale = 0; + } + return new BigDecimal(res, (byte)scale); + } + + public BigDecimal Div(BigDecimal value, out bool overflow) + { + int scale = Scale - value.Scale; + var dividend = Integer; + if (scale < 0) + { + dividend *= Pow10[-scale]; + scale = 0; + } + + var quo = BigInteger.DivRem(dividend, value.Integer, out var remainder); + if (remainder.IsZero) + overflow = BigInteger.Abs(quo) > MaxInteger; + else + { + // We have computed a quotient based on the natural scale ( - ). + // We have a non-zero remainder, so now we increase the scale to DEC_SCALE_MAX+1 to include more quotient bits. + var pow = Pow10[29 - scale]; + quo *= pow; + quo += BigInteger.DivRem(remainder * pow, value.Integer, out remainder); + scale = 29; + + overflow = ScaleResult(ref quo, ref scale, !remainder.IsZero); + + // Unscale the result (removes extra zeroes). + while (scale > 0 && quo.IsEven) + { + var tmp = BigInteger.DivRem(quo, 10, out remainder); + if (!remainder.IsZero) + break; + quo = tmp; + scale--; + } + } + return new BigDecimal(quo, (byte)scale); + } + + static readonly BigInteger MaxInteger = (new BigInteger(ulong.MaxValue) << 32) | uint.MaxValue; + static readonly BigInteger MaxInteger32 = uint.MaxValue; + static readonly double Log2To10 = Math.Log(2) / Math.Log(10); + + /// + /// Returns Log10 for the given number, offset by 96bits. + /// + static int ScaleOverMaxInteger(BigInteger abs) => abs.IsZero ? -28 : (int)(((int)BigInteger.Log(abs, 2) - 95) * Log2To10); + + /// + /// See if we need to scale the result to fit it in 96 bits. + /// Perform needed scaling. Adjust scale factor accordingly. + /// + static bool ScaleResult(ref BigInteger res, ref int scale, bool sticky = false) + { + int newScale = 0; + var abs = BigInteger.Abs(res); + if (abs > MaxInteger) + { + // Find the min scale factor to make the result to fit it in 96 bits, 0 - 29. + // This reduces the scale factor of the result. If it exceeds the current scale of the result, we'll overflow. + newScale = Math.Max(1, ScaleOverMaxInteger(abs)); + if (newScale > scale) + return true; + } + // Make sure we scale by enough to bring the current scale factor into valid range. + newScale = Math.Max(newScale, scale - 28); + + if (newScale != 0) + { + // Scale by the power of 10 given by newScale. + // This is not guaranteed to bring the number within 96 bits -- it could be 1 power of 10 short. + scale -= newScale; + var pow = Pow10[newScale]; + while (true) + { + abs = BigInteger.DivRem(abs, pow, out var remainder); + // If we didn't scale enough, divide by 10 more. + if (abs > MaxInteger) + { + if (scale == 0) + return true; + pow = 10; + scale--; + sticky |= !remainder.IsZero; + continue; + } + + // Round final result. See if remainder >= 1/2 of divisor. + // If remainder == 1/2 divisor, round up if odd or sticky bit set. + pow >>= 1; + if (remainder < pow || remainder == pow && !sticky && abs.IsEven) + break; + if (++abs <= MaxInteger) + break; + + // The rounding caused us to carry beyond 96 bits. Scale by 10 more. + if (scale == 0) + return true; + pow = 10; + scale--; + sticky = false; + } + res = res.Sign < 0 ? -abs : abs; + } + return false; + } + } } }