package math import ( "math/big" "math/rand" "testing" "time" ) // TestDecimalPrecisionPreservation tests that decimal operations preserve precision func TestDecimalPrecisionPreservation(t *testing.T) { dc := NewDecimalConverter() testCases := []struct { name string value string decimals uint8 symbol string }{ {"ETH precision", "1000000000000000000", 18, "ETH"}, // 1 ETH {"USDC precision", "1000000", 6, "USDC"}, // 1 USDC {"WBTC precision", "100000000", 8, "WBTC"}, // 1 WBTC {"Small amount", "1", 18, "ETH"}, // 1 wei {"Large amount", "1000000000000000000000", 18, "ETH"}, // 1000 ETH } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Create decimal from string decimal, err := dc.FromString(tc.value, tc.decimals, tc.symbol) if err != nil { t.Fatalf("Failed to create decimal: %v", err) } // Convert to string and back humanReadable := dc.ToHumanReadable(decimal) backToDecimal, err := dc.FromString(humanReadable, tc.decimals, tc.symbol) if err != nil { t.Fatalf("Failed to convert back from string: %v", err) } // Compare values if decimal.Value.Cmp(backToDecimal.Value) != 0 { t.Errorf("Precision lost in round-trip conversion") t.Errorf("Original: %s", decimal.Value.String()) t.Errorf("Round-trip: %s", backToDecimal.Value.String()) t.Errorf("Human readable: %s", humanReadable) } }) } } // TestArithmeticOperations tests basic arithmetic with different decimal precisions func TestArithmeticOperations(t *testing.T) { dc := NewDecimalConverter() // Create test values with different precisions eth1, _ := dc.FromString("1000000000000000000", 18, "ETH") // 1 ETH eth2, _ := dc.FromString("2000000000000000000", 18, "ETH") // 2 ETH tests := []struct { name string op string a, b *UniversalDecimal expected string }{ { name: "ETH addition", op: "add", a: eth1, b: eth2, expected: "3000000000000000000", // 3 ETH }, { name: "ETH subtraction", op: "sub", a: eth2, b: eth1, expected: "1000000000000000000", // 1 ETH }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var result *UniversalDecimal var err error switch test.op { case "add": result, err = dc.Add(test.a, test.b) case "sub": result, err = dc.Subtract(test.a, test.b) default: t.Fatalf("Unknown operation: %s", test.op) } if err != nil { t.Fatalf("Operation failed: %v", err) } if result.Value.String() != test.expected { t.Errorf("Expected %s, got %s", test.expected, result.Value.String()) } }) } } // TestPercentageCalculations tests percentage calculations for precision func TestPercentageCalculations(t *testing.T) { dc := NewDecimalConverter() testCases := []struct { name string numerator string denominator string decimals uint8 expectedRange [2]float64 // [min, max] acceptable range }{ { name: "1% calculation", numerator: "10000000000000000", // 0.01 ETH denominator: "1000000000000000000", // 1 ETH decimals: 18, expectedRange: [2]float64{0.99, 1.01}, // 1% ± 0.01% }, { name: "50% calculation", numerator: "500000000000000000", // 0.5 ETH denominator: "1000000000000000000", // 1 ETH decimals: 18, expectedRange: [2]float64{49.9, 50.1}, // 50% ± 0.1% }, { name: "Small percentage", numerator: "1000000000000000", // 0.001 ETH denominator: "1000000000000000000", // 1 ETH decimals: 18, expectedRange: [2]float64{0.099, 0.101}, // 0.1% ± 0.001% }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { num, err := dc.FromString(tc.numerator, tc.decimals, "ETH") if err != nil { t.Fatalf("Failed to create numerator: %v", err) } denom, err := dc.FromString(tc.denominator, tc.decimals, "ETH") if err != nil { t.Fatalf("Failed to create denominator: %v", err) } percentage, err := dc.CalculatePercentage(num, denom) if err != nil { t.Fatalf("Failed to calculate percentage: %v", err) } percentageFloat, _ := percentage.Value.Float64() // Convert from raw value to actual percentage (divide by 10^decimals) // Since percentage has 4 decimals, divide by 10000 to get actual percentage value actualPercentage := percentageFloat / 10000.0 t.Logf("Calculated percentage: %.6f%%", actualPercentage) if actualPercentage < tc.expectedRange[0] || actualPercentage > tc.expectedRange[1] { t.Errorf("Percentage %.6f%% outside expected range [%.3f%%, %.3f%%]", actualPercentage, tc.expectedRange[0], tc.expectedRange[1]) } }) } } // PropertyTest tests mathematical properties like commutativity, associativity func TestMathematicalProperties(t *testing.T) { dc := NewDecimalConverter() rand.Seed(time.Now().UnixNano()) // Generate random test values for i := 0; i < 100; i++ { // Generate random big integers val1 := big.NewInt(rand.Int63n(1000000000000000000)) // Up to 1 ETH val2 := big.NewInt(rand.Int63n(1000000000000000000)) val3 := big.NewInt(rand.Int63n(1000000000000000000)) a, _ := NewUniversalDecimal(val1, 18, "ETH") b, _ := NewUniversalDecimal(val2, 18, "ETH") c, _ := NewUniversalDecimal(val3, 18, "ETH") // Test commutativity: a + b = b + a ab, err1 := dc.Add(a, b) ba, err2 := dc.Add(b, a) if err1 != nil || err2 != nil { t.Fatalf("Addition failed: %v, %v", err1, err2) } if ab.Value.Cmp(ba.Value) != 0 { t.Errorf("Addition not commutative: %s + %s = %s, %s + %s = %s", a.Value.String(), b.Value.String(), ab.Value.String(), b.Value.String(), a.Value.String(), ba.Value.String()) } // Test associativity: (a + b) + c = a + (b + c) ab_c, err1 := dc.Add(ab, c) bc, err2 := dc.Add(b, c) a_bc, err3 := dc.Add(a, bc) if err1 != nil || err2 != nil || err3 != nil { continue // Skip this iteration if any operation fails } if ab_c.Value.Cmp(a_bc.Value) != 0 { t.Errorf("Addition not associative") } } } // BenchmarkDecimalOperations benchmarks decimal operations func BenchmarkDecimalOperations(b *testing.B) { dc := NewDecimalConverter() val1, _ := dc.FromString("1000000000000000000", 18, "ETH") val2, _ := dc.FromString("2000000000000000000", 18, "ETH") b.Run("Addition", func(b *testing.B) { for i := 0; i < b.N; i++ { dc.Add(val1, val2) } }) b.Run("Subtraction", func(b *testing.B) { for i := 0; i < b.N; i++ { dc.Subtract(val2, val1) } }) b.Run("Percentage", func(b *testing.B) { for i := 0; i < b.N; i++ { dc.CalculatePercentage(val1, val2) } }) } // FuzzDecimalOperations fuzzes decimal operations for edge cases func FuzzDecimalOperations(f *testing.F) { // Seed with known values f.Add(int64(1000000000000000000), int64(2000000000000000000)) // 1 ETH, 2 ETH f.Add(int64(1), int64(1000000000000000000)) // 1 wei, 1 ETH f.Add(int64(0), int64(1000000000000000000)) // 0, 1 ETH f.Fuzz(func(t *testing.T, val1, val2 int64) { // Ensure positive values if val1 < 0 { val1 = -val1 } if val2 <= 0 { return // Skip zero/negative denominators } dc := NewDecimalConverter() a, err := NewUniversalDecimal(big.NewInt(val1), 18, "ETH") if err != nil { return } b, err := NewUniversalDecimal(big.NewInt(val2), 18, "ETH") if err != nil { return } // Test addition doesn't panic _, err = dc.Add(a, b) if err != nil { t.Errorf("Addition failed: %v", err) } // Test subtraction doesn't panic (if a >= b) if val1 >= val2 { _, err = dc.Subtract(a, b) if err != nil { t.Errorf("Subtraction failed: %v", err) } } // Test percentage calculation doesn't panic _, err = dc.CalculatePercentage(a, b) if err != nil { t.Errorf("Percentage calculation failed: %v", err) } }) }