/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "encoding/json" "fmt" "math" "math/rand" "strings" "testing" "unicode" fuzz "github.com/google/gofuzz" inf "gopkg.in/inf.v0" ) func dec(i int64, exponent int) infDecAmount { // See the below test-- scale is the negative of an exponent. return infDecAmount{inf.NewDec(i, inf.Scale(-exponent))} } func decQuantity(i int64, exponent int, format Format) Quantity { return Quantity{d: dec(i, exponent), Format: format} } func intQuantity(i int64, exponent Scale, format Format) Quantity { return Quantity{i: int64Amount{value: i, scale: exponent}, Format: format} } func TestDec(t *testing.T) { table := []struct { got infDecAmount expect string }{ {dec(1, 0), "1"}, {dec(1, 1), "10"}, {dec(5, 2), "500"}, {dec(8, 3), "8000"}, {dec(2, 0), "2"}, {dec(1, -1), "0.1"}, {dec(3, -2), "0.03"}, {dec(4, -3), "0.004"}, } for _, item := range table { if e, a := item.expect, item.got.Dec.String(); e != a { t.Errorf("expected %v, got %v", e, a) } } } // TestQuantityParseZero ensures that when a 0 quantity is passed, its string value is 0 func TestQuantityParseZero(t *testing.T) { zero := MustParse("0") if expected, actual := "0", zero.String(); expected != actual { t.Errorf("Expected %v, actual %v", expected, actual) } } // TestQuantityParseNonNumericPanic ensures that when a non-numeric string is parsed // it panics func TestQuantityParseNonNumericPanic(t *testing.T) { defer func() { if r := recover(); r == nil { t.Errorf("MustParse did not panic") } }() _ = MustParse("Non-Numeric") } // TestQuantityAddZeroPreservesSuffix verifies that a suffix is preserved // independent of the order of operations when adding a zero and non-zero val func TestQuantityAddZeroPreservesSuffix(t *testing.T) { testValues := []string{"100m", "1Gi"} zero := MustParse("0") for _, testValue := range testValues { value := MustParse(testValue) v1 := value.DeepCopy() // ensure non-zero + zero = non-zero (suffix preserved) v1.Add(zero) // ensure zero + non-zero = non-zero (suffix preserved) v2 := zero.DeepCopy() v2.Add(value) if v1.String() != testValue { t.Errorf("Expected %v, actual %v", testValue, v1.String()) continue } if v2.String() != testValue { t.Errorf("Expected %v, actual %v", testValue, v2.String()) } } } // TestQuantitySubZeroPreservesSuffix verifies that a suffix is preserved // independent of the order of operations when subtracting a zero and non-zero val func TestQuantitySubZeroPreservesSuffix(t *testing.T) { testValues := []string{"100m", "1Gi"} zero := MustParse("0") for _, testValue := range testValues { value := MustParse(testValue) v1 := value.DeepCopy() // ensure non-zero - zero = non-zero (suffix preserved) v1.Sub(zero) // ensure we preserved the input value if v1.String() != testValue { t.Errorf("Expected %v, actual %v", testValue, v1.String()) } // ensure zero - non-zero = -non-zero (suffix preserved) v2 := zero.DeepCopy() v2.Sub(value) negVal := value.DeepCopy() negVal.Neg() if v2.String() != negVal.String() { t.Errorf("Expected %v, actual %v", negVal.String(), v2.String()) } } } // TestQuantityCanocicalizeZero verifies that you get 0 as canonical value if internal value is 0, and not 0 func TestQuantityCanocicalizeZero(t *testing.T) { val := MustParse("1000m") val.i.Sub(int64Amount{value: 1}) zero := Quantity{i: val.i, Format: DecimalSI} if expected, actual := "0", zero.String(); expected != actual { t.Errorf("Expected %v, actual %v", expected, actual) } } func TestQuantityCmp(t *testing.T) { // Test when d is nil table := []struct { x string y string expect int }{ {"0", "0", 0}, {"100m", "50m", 1}, {"50m", "100m", -1}, {"10000T", "100Gi", 1}, } for _, testCase := range table { q1 := MustParse(testCase.x) q2 := MustParse(testCase.y) if result := q1.Cmp(q2); result != testCase.expect { t.Errorf("X: %v, Y: %v, Expected: %v, Actual: %v", testCase.x, testCase.y, testCase.expect, result) } } // Test when i is {0,0} table2 := []struct { x *inf.Dec y *inf.Dec expect int }{ {dec(0, 0).Dec, dec(0, 0).Dec, 0}, {nil, dec(0, 0).Dec, 0}, {dec(0, 0).Dec, nil, 0}, {nil, nil, 0}, {nil, dec(10, 0).Dec, -1}, {nil, dec(-10, 0).Dec, 1}, {dec(10, 0).Dec, nil, 1}, {dec(-10, 0).Dec, nil, -1}, } for _, testCase := range table2 { q1 := Quantity{d: infDecAmount{testCase.x}, Format: DecimalSI} q2 := Quantity{d: infDecAmount{testCase.y}, Format: DecimalSI} if result := q1.Cmp(q2); result != testCase.expect { t.Errorf("X: %v, Y: %v, Expected: %v, Actual: %v", testCase.x, testCase.y, testCase.expect, result) } } } func TestParseQuantityString(t *testing.T) { table := []struct { input string positive bool value string num, denom, suffix string }{ {"0.025Ti", true, "0.025", "0", "025", "Ti"}, {"1.025Ti", true, "1.025", "1", "025", "Ti"}, {"-1.025Ti", false, "-1.025", "1", "025", "Ti"}, {".", true, ".", "0", "", ""}, {"-.", false, "-.", "0", "", ""}, {"1E-3", true, "1", "1", "", "E-3"}, } for _, test := range table { positive, value, num, denom, suffix, err := parseQuantityString(test.input) if err != nil { t.Errorf("%s: error: %v", test.input, err) continue } if positive != test.positive || value != test.value || num != test.num || denom != test.denom || suffix != test.suffix { t.Errorf("%s: unmatched: %t %q %q %q %q", test.input, positive, value, num, denom, suffix) } } } func TestQuantityParse(t *testing.T) { if _, err := ParseQuantity(""); err == nil { t.Errorf("expected empty string to return error") } table := []struct { input string expect Quantity }{ {"0", decQuantity(0, 0, DecimalSI)}, {"0n", decQuantity(0, 0, DecimalSI)}, {"0u", decQuantity(0, 0, DecimalSI)}, {"0m", decQuantity(0, 0, DecimalSI)}, {"0Ki", decQuantity(0, 0, BinarySI)}, {"0k", decQuantity(0, 0, DecimalSI)}, {"0Mi", decQuantity(0, 0, BinarySI)}, {"0M", decQuantity(0, 0, DecimalSI)}, {"0Gi", decQuantity(0, 0, BinarySI)}, {"0G", decQuantity(0, 0, DecimalSI)}, {"0Ti", decQuantity(0, 0, BinarySI)}, {"0T", decQuantity(0, 0, DecimalSI)}, // Quantity less numbers are allowed {"1", decQuantity(1, 0, DecimalSI)}, // Binary suffixes {"1Ki", decQuantity(1024, 0, BinarySI)}, {"8Ki", decQuantity(8*1024, 0, BinarySI)}, {"7Mi", decQuantity(7*1024*1024, 0, BinarySI)}, {"6Gi", decQuantity(6*1024*1024*1024, 0, BinarySI)}, {"5Ti", decQuantity(5*1024*1024*1024*1024, 0, BinarySI)}, {"4Pi", decQuantity(4*1024*1024*1024*1024*1024, 0, BinarySI)}, {"3Ei", decQuantity(3*1024*1024*1024*1024*1024*1024, 0, BinarySI)}, {"10Ti", decQuantity(10*1024*1024*1024*1024, 0, BinarySI)}, {"100Ti", decQuantity(100*1024*1024*1024*1024, 0, BinarySI)}, // Decimal suffixes {"5n", decQuantity(5, -9, DecimalSI)}, {"4u", decQuantity(4, -6, DecimalSI)}, {"3m", decQuantity(3, -3, DecimalSI)}, {"9", decQuantity(9, 0, DecimalSI)}, {"8k", decQuantity(8, 3, DecimalSI)}, {"50k", decQuantity(5, 4, DecimalSI)}, {"7M", decQuantity(7, 6, DecimalSI)}, {"6G", decQuantity(6, 9, DecimalSI)}, {"5T", decQuantity(5, 12, DecimalSI)}, {"40T", decQuantity(4, 13, DecimalSI)}, {"300T", decQuantity(3, 14, DecimalSI)}, {"2P", decQuantity(2, 15, DecimalSI)}, {"1E", decQuantity(1, 18, DecimalSI)}, // Decimal exponents {"1E-3", decQuantity(1, -3, DecimalExponent)}, {"1e3", decQuantity(1, 3, DecimalExponent)}, {"1E6", decQuantity(1, 6, DecimalExponent)}, {"1e9", decQuantity(1, 9, DecimalExponent)}, {"1E12", decQuantity(1, 12, DecimalExponent)}, {"1e15", decQuantity(1, 15, DecimalExponent)}, {"1E18", decQuantity(1, 18, DecimalExponent)}, // Nonstandard but still parsable {"1e14", decQuantity(1, 14, DecimalExponent)}, {"1e13", decQuantity(1, 13, DecimalExponent)}, {"1e3", decQuantity(1, 3, DecimalExponent)}, {"100.035k", decQuantity(100035, 0, DecimalSI)}, // Things that look like floating point {"0.001", decQuantity(1, -3, DecimalSI)}, {"0.0005k", decQuantity(5, -1, DecimalSI)}, {"0.005", decQuantity(5, -3, DecimalSI)}, {"0.05", decQuantity(5, -2, DecimalSI)}, {"0.5", decQuantity(5, -1, DecimalSI)}, {"0.00050k", decQuantity(5, -1, DecimalSI)}, {"0.00500", decQuantity(5, -3, DecimalSI)}, {"0.05000", decQuantity(5, -2, DecimalSI)}, {"0.50000", decQuantity(5, -1, DecimalSI)}, {"0.5e0", decQuantity(5, -1, DecimalExponent)}, {"0.5e-1", decQuantity(5, -2, DecimalExponent)}, {"0.5e-2", decQuantity(5, -3, DecimalExponent)}, {"0.5e0", decQuantity(5, -1, DecimalExponent)}, {"10.035M", decQuantity(10035, 3, DecimalSI)}, {"1.2e3", decQuantity(12, 2, DecimalExponent)}, {"1.3E+6", decQuantity(13, 5, DecimalExponent)}, {"1.40e9", decQuantity(14, 8, DecimalExponent)}, {"1.53E12", decQuantity(153, 10, DecimalExponent)}, {"1.6e15", decQuantity(16, 14, DecimalExponent)}, {"1.7E18", decQuantity(17, 17, DecimalExponent)}, {"9.01", decQuantity(901, -2, DecimalSI)}, {"8.1k", decQuantity(81, 2, DecimalSI)}, {"7.123456M", decQuantity(7123456, 0, DecimalSI)}, {"6.987654321G", decQuantity(6987654321, 0, DecimalSI)}, {"5.444T", decQuantity(5444, 9, DecimalSI)}, {"40.1T", decQuantity(401, 11, DecimalSI)}, {"300.2T", decQuantity(3002, 11, DecimalSI)}, {"2.5P", decQuantity(25, 14, DecimalSI)}, {"1.01E", decQuantity(101, 16, DecimalSI)}, // Things that saturate/round {"3.001n", decQuantity(4, -9, DecimalSI)}, {"1.1E-9", decQuantity(2, -9, DecimalExponent)}, {"0.0000000001", decQuantity(1, -9, DecimalSI)}, {"0.0000000005", decQuantity(1, -9, DecimalSI)}, {"0.00000000050", decQuantity(1, -9, DecimalSI)}, {"0.5e-9", decQuantity(1, -9, DecimalExponent)}, {"0.9n", decQuantity(1, -9, DecimalSI)}, {"0.00000012345", decQuantity(124, -9, DecimalSI)}, {"0.00000012354", decQuantity(124, -9, DecimalSI)}, {"9Ei", Quantity{d: maxAllowed, Format: BinarySI}}, {"9223372036854775807Ki", Quantity{d: maxAllowed, Format: BinarySI}}, {"12E", decQuantity(12, 18, DecimalSI)}, // We'll accept fractional binary stuff, too. {"100.035Ki", decQuantity(10243584, -2, BinarySI)}, {"0.5Mi", decQuantity(.5*1024*1024, 0, BinarySI)}, {"0.05Gi", decQuantity(536870912, -1, BinarySI)}, {"0.025Ti", decQuantity(274877906944, -1, BinarySI)}, // Things written by trolls {"0.000000000001Ki", decQuantity(2, -9, DecimalSI)}, // rounds up, changes format {".001", decQuantity(1, -3, DecimalSI)}, {".0001k", decQuantity(100, -3, DecimalSI)}, {"1.", decQuantity(1, 0, DecimalSI)}, {"1.G", decQuantity(1, 9, DecimalSI)}, } for _, asDec := range []bool{false, true} { for _, item := range table { got, err := ParseQuantity(item.input) if err != nil { t.Errorf("%v: unexpected error: %v", item.input, err) continue } if asDec { got.AsDec() } if e, a := item.expect, got; e.Cmp(a) != 0 { t.Errorf("%v: expected %v, got %v", item.input, e.String(), a.String()) } if e, a := item.expect.Format, got.Format; e != a { t.Errorf("%v: expected %#v, got %#v", item.input, e, a) } if asDec { if i, ok := got.AsInt64(); i != 0 || ok { t.Errorf("%v: expected inf.Dec to return false for AsInt64: %d", item.input, i) } continue } i, ok := item.expect.AsInt64() if !ok { continue } j, ok := got.AsInt64() if !ok { if got.d.Dec == nil && got.i.scale >= 0 { t.Errorf("%v: is an int64Amount, but can't return AsInt64: %v", item.input, got) } continue } if i != j { t.Errorf("%v: expected equivalent representation as int64: %d %d", item.input, i, j) } } for _, item := range table { got, err := ParseQuantity(item.input) if err != nil { t.Errorf("%v: unexpected error: %v", item.input, err) continue } if asDec { got.AsDec() } // verify that we can decompose the input and get the same result by building up from the base. positive, _, num, denom, suffix, err := parseQuantityString(item.input) if err != nil { t.Errorf("%v: unexpected error: %v", item.input, err) continue } if got.Sign() >= 0 && !positive || got.Sign() < 0 && positive { t.Errorf("%v: positive was incorrect: %t", item.input, positive) continue } var value string if !positive { value = "-" } value += num if len(denom) > 0 { value += "." + denom } value += suffix if len(value) == 0 { t.Errorf("%v: did not parse correctly, %q %q %q", item.input, num, denom, suffix) } expected, err := ParseQuantity(value) if err != nil { t.Errorf("%v: unexpected error for %s: %v", item.input, value, err) continue } if expected.Cmp(got) != 0 { t.Errorf("%v: not the same as %s", item.input, value) continue } } // Try the negative version of everything desired := &inf.Dec{} expect := Quantity{d: infDecAmount{Dec: desired}} for _, item := range table { got, err := ParseQuantity("-" + strings.TrimLeftFunc(item.input, unicode.IsSpace)) if err != nil { t.Errorf("-%v: unexpected error: %v", item.input, err) continue } if asDec { got.AsDec() } expected := item.expect desired.Neg(expected.AsDec()) if e, a := expect, got; e.Cmp(a) != 0 { t.Errorf("%v: expected %s, got %s", item.input, e.String(), a.String()) } if e, a := expected.Format, got.Format; e != a { t.Errorf("%v: expected %#v, got %#v", item.input, e, a) } } // Try everything with an explicit + for _, item := range table { got, err := ParseQuantity("+" + strings.TrimLeftFunc(item.input, unicode.IsSpace)) if err != nil { t.Errorf("-%v: unexpected error: %v", item.input, err) continue } if asDec { got.AsDec() } if e, a := item.expect, got; e.Cmp(a) != 0 { t.Errorf("%v(%t): expected %s, got %s", item.input, asDec, e.String(), a.String()) } if e, a := item.expect.Format, got.Format; e != a { t.Errorf("%v: expected %#v, got %#v", item.input, e, a) } } } invalid := []string{ "1.1.M", "1+1.0M", "0.1mi", "0.1am", "aoeu", ".5i", "1i", "-3.01i", "-3.01e-", // trailing whitespace is forbidden " 1", "1 ", } for _, item := range invalid { _, err := ParseQuantity(item) if err == nil { t.Errorf("%v parsed unexpectedly", item) } } } func TestQuantityRoundUp(t *testing.T) { table := []struct { in string scale Scale expect Quantity ok bool }{ {"9.01", -3, decQuantity(901, -2, DecimalSI), true}, {"9.01", -2, decQuantity(901, -2, DecimalSI), true}, {"9.01", -1, decQuantity(91, -1, DecimalSI), false}, {"9.01", 0, decQuantity(10, 0, DecimalSI), false}, {"9.01", 1, decQuantity(10, 0, DecimalSI), false}, {"9.01", 2, decQuantity(100, 0, DecimalSI), false}, {"-9.01", -3, decQuantity(-901, -2, DecimalSI), true}, {"-9.01", -2, decQuantity(-901, -2, DecimalSI), true}, {"-9.01", -1, decQuantity(-91, -1, DecimalSI), false}, {"-9.01", 0, decQuantity(-10, 0, DecimalSI), false}, {"-9.01", 1, decQuantity(-10, 0, DecimalSI), false}, {"-9.01", 2, decQuantity(-100, 0, DecimalSI), false}, } for _, asDec := range []bool{false, true} { for _, item := range table { got, err := ParseQuantity(item.in) if err != nil { t.Fatalf("unexpected error: %v", err) } expect := item.expect.DeepCopy() if asDec { got.AsDec() } if ok := got.RoundUp(item.scale); ok != item.ok { t.Errorf("%s(%d,%t): unexpected ok: %t", item.in, item.scale, asDec, ok) } if got.Cmp(expect) != 0 { t.Errorf("%s(%d,%t): unexpected round: %s vs %s", item.in, item.scale, asDec, got.String(), expect.String()) } } } } func TestQuantityCmpInt64AndDec(t *testing.T) { table := []struct { a, b Quantity cmp int }{ {intQuantity(901, -2, DecimalSI), intQuantity(901, -2, DecimalSI), 0}, {intQuantity(90, -1, DecimalSI), intQuantity(901, -2, DecimalSI), -1}, {intQuantity(901, -2, DecimalSI), intQuantity(900, -2, DecimalSI), 1}, {intQuantity(0, 0, DecimalSI), intQuantity(0, 0, DecimalSI), 0}, {intQuantity(0, 1, DecimalSI), intQuantity(0, -1, DecimalSI), 0}, {intQuantity(0, -1, DecimalSI), intQuantity(0, 1, DecimalSI), 0}, {intQuantity(800, -3, DecimalSI), intQuantity(1, 0, DecimalSI), -1}, {intQuantity(800, -3, DecimalSI), intQuantity(79, -2, DecimalSI), 1}, {intQuantity(mostPositive, 0, DecimalSI), intQuantity(1, -1, DecimalSI), 1}, {intQuantity(mostPositive, 1, DecimalSI), intQuantity(1, 0, DecimalSI), 1}, {intQuantity(mostPositive, 1, DecimalSI), intQuantity(1, 1, DecimalSI), 1}, {intQuantity(mostPositive, 1, DecimalSI), intQuantity(0, 1, DecimalSI), 1}, {intQuantity(mostPositive, -16, DecimalSI), intQuantity(1, 3, DecimalSI), -1}, {intQuantity(mostNegative, 0, DecimalSI), intQuantity(0, 0, DecimalSI), -1}, {intQuantity(mostNegative, -18, DecimalSI), intQuantity(-1, 0, DecimalSI), -1}, {intQuantity(mostNegative, -19, DecimalSI), intQuantity(-1, 0, DecimalSI), 1}, {intQuantity(1*1000000*1000000*1000000, -17, DecimalSI), intQuantity(1, 1, DecimalSI), 0}, {intQuantity(1*1000000*1000000*1000000, -17, DecimalSI), intQuantity(-10, 0, DecimalSI), 1}, {intQuantity(-1*1000000*1000000*1000000, -17, DecimalSI), intQuantity(-10, 0, DecimalSI), 0}, {intQuantity(1*1000000*1000000*1000000, -17, DecimalSI), intQuantity(1, 0, DecimalSI), 1}, {intQuantity(1*1000000*1000000*1000000+1, -17, DecimalSI), intQuantity(1, 1, DecimalSI), 1}, {intQuantity(1*1000000*1000000*1000000-1, -17, DecimalSI), intQuantity(1, 1, DecimalSI), -1}, } for _, item := range table { if cmp := item.a.Cmp(item.b); cmp != item.cmp { t.Errorf("%#v: unexpected Cmp: %d", item, cmp) } if cmp := item.b.Cmp(item.a); cmp != -item.cmp { t.Errorf("%#v: unexpected inverted Cmp: %d", item, cmp) } } for _, item := range table { a, b := item.a.DeepCopy(), item.b.DeepCopy() a.AsDec() if cmp := a.Cmp(b); cmp != item.cmp { t.Errorf("%#v: unexpected Cmp: %d", item, cmp) } if cmp := b.Cmp(a); cmp != -item.cmp { t.Errorf("%#v: unexpected inverted Cmp: %d", item, cmp) } } for _, item := range table { a, b := item.a.DeepCopy(), item.b.DeepCopy() b.AsDec() if cmp := a.Cmp(b); cmp != item.cmp { t.Errorf("%#v: unexpected Cmp: %d", item, cmp) } if cmp := b.Cmp(a); cmp != -item.cmp { t.Errorf("%#v: unexpected inverted Cmp: %d", item, cmp) } } for _, item := range table { a, b := item.a.DeepCopy(), item.b.DeepCopy() a.AsDec() b.AsDec() if cmp := a.Cmp(b); cmp != item.cmp { t.Errorf("%#v: unexpected Cmp: %d", item, cmp) } if cmp := b.Cmp(a); cmp != -item.cmp { t.Errorf("%#v: unexpected inverted Cmp: %d", item, cmp) } } } func TestQuantityNeg(t *testing.T) { table := []struct { a Quantity out string }{ {intQuantity(901, -2, DecimalSI), "-9010m"}, {decQuantity(901, -2, DecimalSI), "-9010m"}, } for i, item := range table { out := item.a.DeepCopy() out.Neg() if out.Cmp(item.a) == 0 { t.Errorf("%d: negating an item should not mutate the source: %s", i, out.String()) } if out.String() != item.out { t.Errorf("%d: negating did not equal exact value: %s", i, out.String()) } } } func TestQuantityString(t *testing.T) { table := []struct { in Quantity expect string alternate string }{ {decQuantity(1024*1024*1024, 0, BinarySI), "1Gi", "1024Mi"}, {decQuantity(300*1024*1024, 0, BinarySI), "300Mi", "307200Ki"}, {decQuantity(6*1024, 0, BinarySI), "6Ki", ""}, {decQuantity(1001*1024*1024*1024, 0, BinarySI), "1001Gi", "1025024Mi"}, {decQuantity(1024*1024*1024*1024, 0, BinarySI), "1Ti", "1024Gi"}, {decQuantity(5, 0, BinarySI), "5", "5000m"}, {decQuantity(500, -3, BinarySI), "500m", "0.5"}, {decQuantity(1, 9, DecimalSI), "1G", "1000M"}, {decQuantity(1000, 6, DecimalSI), "1G", "0.001T"}, {decQuantity(1000000, 3, DecimalSI), "1G", ""}, {decQuantity(1000000000, 0, DecimalSI), "1G", ""}, {decQuantity(1, -3, DecimalSI), "1m", "1000u"}, {decQuantity(80, -3, DecimalSI), "80m", ""}, {decQuantity(1080, -3, DecimalSI), "1080m", "1.08"}, {decQuantity(108, -2, DecimalSI), "1080m", "1080000000n"}, {decQuantity(10800, -4, DecimalSI), "1080m", ""}, {decQuantity(300, 6, DecimalSI), "300M", ""}, {decQuantity(1, 12, DecimalSI), "1T", ""}, {decQuantity(1234567, 6, DecimalSI), "1234567M", ""}, {decQuantity(1234567, -3, BinarySI), "1234567m", ""}, {decQuantity(3, 3, DecimalSI), "3k", ""}, {decQuantity(1025, 0, BinarySI), "1025", ""}, {decQuantity(0, 0, DecimalSI), "0", ""}, {decQuantity(0, 0, BinarySI), "0", ""}, {decQuantity(1, 9, DecimalExponent), "1e9", ".001e12"}, {decQuantity(1, -3, DecimalExponent), "1e-3", "0.001e0"}, {decQuantity(1, -9, DecimalExponent), "1e-9", "1000e-12"}, {decQuantity(80, -3, DecimalExponent), "80e-3", ""}, {decQuantity(300, 6, DecimalExponent), "300e6", ""}, {decQuantity(1, 12, DecimalExponent), "1e12", ""}, {decQuantity(1, 3, DecimalExponent), "1e3", ""}, {decQuantity(3, 3, DecimalExponent), "3e3", ""}, {decQuantity(3, 3, DecimalSI), "3k", ""}, {decQuantity(0, 0, DecimalExponent), "0", "00"}, {decQuantity(1, -9, DecimalSI), "1n", ""}, {decQuantity(80, -9, DecimalSI), "80n", ""}, {decQuantity(1080, -9, DecimalSI), "1080n", ""}, {decQuantity(108, -8, DecimalSI), "1080n", ""}, {decQuantity(10800, -10, DecimalSI), "1080n", ""}, {decQuantity(1, -6, DecimalSI), "1u", ""}, {decQuantity(80, -6, DecimalSI), "80u", ""}, {decQuantity(1080, -6, DecimalSI), "1080u", ""}, } for _, item := range table { got := item.in.String() if e, a := item.expect, got; e != a { t.Errorf("%#v: expected %v, got %v", item.in, e, a) } q, err := ParseQuantity(item.expect) if err != nil { t.Errorf("%#v: unexpected error: %v", item.expect, err) } if len(q.s) == 0 || q.s != item.expect { t.Errorf("%#v: did not copy canonical string on parse: %s", item.expect, q.s) } if len(item.alternate) == 0 { continue } q, err = ParseQuantity(item.alternate) if err != nil { t.Errorf("%#v: unexpected error: %v", item.expect, err) continue } if len(q.s) != 0 { t.Errorf("%#v: unexpected nested string: %v", item.expect, q.s) } if q.String() != item.expect { t.Errorf("%#v: unexpected alternate canonical: %v", item.expect, q.String()) } if len(q.s) == 0 || q.s != item.expect { t.Errorf("%#v: did not set canonical string on ToString: %s", item.expect, q.s) } } desired := &inf.Dec{} // Avoid modifying the values in the table. for _, item := range table { if item.in.Cmp(Quantity{}) == 0 { // Don't expect it to print "-0" ever continue } q := item.in q.d = infDecAmount{desired.Neg(q.AsDec())} if e, a := "-"+item.expect, q.String(); e != a { t.Errorf("%#v: expected %v, got %v", item.in, e, a) } } } func TestQuantityParseEmit(t *testing.T) { table := []struct { in string expect string }{ {"1Ki", "1Ki"}, {"1Mi", "1Mi"}, {"1Gi", "1Gi"}, {"1024Mi", "1Gi"}, {"1000M", "1G"}, {".001Ki", "1024m"}, {".000001Ki", "1024u"}, {".000000001Ki", "1024n"}, {".000000000001Ki", "2n"}, } for _, item := range table { q, err := ParseQuantity(item.in) if err != nil { t.Errorf("Couldn't parse %v", item.in) continue } if e, a := item.expect, q.String(); e != a { t.Errorf("%#v: expected %v, got %v", item.in, e, a) } } for _, item := range table { q, err := ParseQuantity("-" + item.in) if err != nil { t.Errorf("Couldn't parse %v", item.in) continue } if q.Cmp(Quantity{}) == 0 { continue } if e, a := "-"+item.expect, q.String(); e != a { t.Errorf("%#v: expected %v, got %v (%#v)", item.in, e, a, q.i) } } } var fuzzer = fuzz.New().Funcs( func(q *Quantity, c fuzz.Continue) { q.i = Zero if c.RandBool() { q.Format = BinarySI if c.RandBool() { dec := &inf.Dec{} q.d = infDecAmount{Dec: dec} dec.SetScale(0) dec.SetUnscaled(c.Int63()) return } // Be sure to test cases like 1Mi dec := &inf.Dec{} q.d = infDecAmount{Dec: dec} dec.SetScale(0) dec.SetUnscaled(c.Int63n(1024) << uint(10*c.Intn(5))) return } if c.RandBool() { q.Format = DecimalSI } else { q.Format = DecimalExponent } if c.RandBool() { dec := &inf.Dec{} q.d = infDecAmount{Dec: dec} dec.SetScale(inf.Scale(c.Intn(4))) dec.SetUnscaled(c.Int63()) return } // Be sure to test cases like 1M dec := &inf.Dec{} q.d = infDecAmount{Dec: dec} dec.SetScale(inf.Scale(3 - c.Intn(15))) dec.SetUnscaled(c.Int63n(1000)) }, ) func TestQuantityDeepCopy(t *testing.T) { // Test when d is nil slice := []string{"0", "100m", "50m", "10000T"} for _, testCase := range slice { q := MustParse(testCase) if result := q.DeepCopy(); result != q { t.Errorf("Expected: %v, Actual: %v", q, result) } } table := []*inf.Dec{ dec(0, 0).Dec, dec(10, 0).Dec, dec(-10, 0).Dec, } // Test when i is {0,0} for _, testCase := range table { q := Quantity{d: infDecAmount{testCase}, Format: DecimalSI} result := q.DeepCopy() if q.d.Cmp(result.AsDec()) != 0 { t.Errorf("Expected: %v, Actual: %v", q.String(), result.String()) } result = Quantity{d: infDecAmount{dec(2, 0).Dec}, Format: DecimalSI} if q.d.Cmp(result.AsDec()) == 0 { t.Errorf("Modifying result has affected q") } } } func TestJSON(t *testing.T) { for i := 0; i < 500; i++ { q := &Quantity{} fuzzer.Fuzz(q) b, err := json.Marshal(q) if err != nil { t.Errorf("error encoding %v: %v", q, err) continue } q2 := &Quantity{} err = json.Unmarshal(b, q2) if err != nil { t.Logf("%d: %s", i, string(b)) t.Errorf("%v: error decoding %v: %v", q, string(b), err) } if q2.Cmp(*q) != 0 { t.Errorf("Expected equal: %v, %v (json was '%v')", q, q2, string(b)) } } } func TestJSONWhitespace(t *testing.T) { q := Quantity{} testCases := []struct { in string expect string }{ {`" 1"`, "1"}, {`"1 "`, "1"}, {`1`, "1"}, {` 1`, "1"}, {`1 `, "1"}, {`10`, "10"}, {`-1`, "-1"}, {` -1`, "-1"}, } for _, test := range testCases { if err := json.Unmarshal([]byte(test.in), &q); err != nil { t.Errorf("%q: %v", test.in, err) } if q.String() != test.expect { t.Errorf("unexpected string: %q", q.String()) } } } func TestMilliNewSet(t *testing.T) { table := []struct { value int64 format Format expect string exact bool }{ {1, DecimalSI, "1m", true}, {1000, DecimalSI, "1", true}, {1234000, DecimalSI, "1234", true}, {1024, BinarySI, "1024m", false}, // Format changes {1000000, "invalidFormatDefaultsToExponent", "1e3", true}, {1024 * 1024, BinarySI, "1048576m", false}, // Format changes } for _, item := range table { q := NewMilliQuantity(item.value, item.format) if e, a := item.expect, q.String(); e != a { t.Errorf("Expected %v, got %v; %#v", e, a, q) } if !item.exact { continue } q2, err := ParseQuantity(q.String()) if err != nil { t.Errorf("Round trip failed on %v", q) } if e, a := item.value, q2.MilliValue(); e != a { t.Errorf("Expected %v, got %v", e, a) } } for _, item := range table { q := NewQuantity(0, item.format) q.SetMilli(item.value) if e, a := item.expect, q.String(); e != a { t.Errorf("Set: Expected %v, got %v; %#v", e, a, q) } } } func TestNewSet(t *testing.T) { table := []struct { value int64 format Format expect string }{ {1, DecimalSI, "1"}, {1000, DecimalSI, "1k"}, {1234000, DecimalSI, "1234k"}, {1024, BinarySI, "1Ki"}, {1000000, "invalidFormatDefaultsToExponent", "1e6"}, {1024 * 1024, BinarySI, "1Mi"}, } for _, asDec := range []bool{false, true} { for _, item := range table { q := NewQuantity(item.value, item.format) if asDec { q.ToDec() } if e, a := item.expect, q.String(); e != a { t.Errorf("Expected %v, got %v; %#v", e, a, q) } q2, err := ParseQuantity(q.String()) if err != nil { t.Errorf("Round trip failed on %v", q) } if e, a := item.value, q2.Value(); e != a { t.Errorf("Expected %v, got %v", e, a) } } for _, item := range table { q := NewQuantity(0, item.format) q.Set(item.value) if asDec { q.ToDec() } if e, a := item.expect, q.String(); e != a { t.Errorf("Set: Expected %v, got %v; %#v", e, a, q) } } } } func TestNewScaledSet(t *testing.T) { table := []struct { value int64 scale Scale expect string }{ {1, Nano, "1n"}, {1000, Nano, "1u"}, {1, Micro, "1u"}, {1000, Micro, "1m"}, {1, Milli, "1m"}, {1000, Milli, "1"}, {1, 0, "1"}, {0, Nano, "0"}, {0, Micro, "0"}, {0, Milli, "0"}, {0, 0, "0"}, } for _, item := range table { q := NewScaledQuantity(item.value, item.scale) if e, a := item.expect, q.String(); e != a { t.Errorf("Expected %v, got %v; %#v", e, a, q) } q2, err := ParseQuantity(q.String()) if err != nil { t.Errorf("Round trip failed on %v", q) } if e, a := item.value, q2.ScaledValue(item.scale); e != a { t.Errorf("Expected %v, got %v", e, a) } q3 := NewQuantity(0, DecimalSI) q3.SetScaled(item.value, item.scale) if q.Cmp(*q3) != 0 { t.Errorf("Expected %v and %v to be equal", q, q3) } } } func TestScaledValue(t *testing.T) { table := []struct { fromScale Scale toScale Scale expected int64 }{ {Nano, Nano, 1}, {Nano, Micro, 1}, {Nano, Milli, 1}, {Nano, 0, 1}, {Micro, Nano, 1000}, {Micro, Micro, 1}, {Micro, Milli, 1}, {Micro, 0, 1}, {Milli, Nano, 1000 * 1000}, {Milli, Micro, 1000}, {Milli, Milli, 1}, {Milli, 0, 1}, {0, Nano, 1000 * 1000 * 1000}, {0, Micro, 1000 * 1000}, {0, Milli, 1000}, {0, 0, 1}, {2, -2, 100 * 100}, } for _, item := range table { q := NewScaledQuantity(1, item.fromScale) if e, a := item.expected, q.ScaledValue(item.toScale); e != a { t.Errorf("%v to %v: Expected %v, got %v", item.fromScale, item.toScale, e, a) } } } func TestUninitializedNoCrash(t *testing.T) { var q Quantity q.Value() q.MilliValue() q.DeepCopy() _ = q.String() q.MarshalJSON() } func TestDeepCopy(t *testing.T) { q := NewQuantity(5, DecimalSI) c := q.DeepCopy() c.Set(6) if q.Value() == 6 { t.Errorf("Copy didn't") } } func TestSub(t *testing.T) { tests := []struct { a Quantity b Quantity expected Quantity }{ {decQuantity(10, 0, DecimalSI), decQuantity(1, 1, DecimalSI), decQuantity(0, 0, DecimalSI)}, {decQuantity(10, 0, DecimalSI), decQuantity(1, 0, BinarySI), decQuantity(9, 0, DecimalSI)}, {decQuantity(10, 0, BinarySI), decQuantity(1, 0, DecimalSI), decQuantity(9, 0, BinarySI)}, {Quantity{Format: DecimalSI}, decQuantity(50, 0, DecimalSI), decQuantity(-50, 0, DecimalSI)}, {decQuantity(50, 0, DecimalSI), Quantity{Format: DecimalSI}, decQuantity(50, 0, DecimalSI)}, {Quantity{Format: DecimalSI}, Quantity{Format: DecimalSI}, decQuantity(0, 0, DecimalSI)}, } for i, test := range tests { test.a.Sub(test.b) if test.a.Cmp(test.expected) != 0 { t.Errorf("[%d] Expected %q, got %q", i, test.expected.String(), test.a.String()) } } } func TestNeg(t *testing.T) { tests := []struct { a Quantity b Quantity expected Quantity }{ {a: intQuantity(0, 0, DecimalSI), expected: intQuantity(0, 0, DecimalSI)}, {a: Quantity{}, expected: Quantity{}}, {a: intQuantity(10, 0, BinarySI), expected: intQuantity(-10, 0, BinarySI)}, {a: intQuantity(-10, 0, BinarySI), expected: intQuantity(10, 0, BinarySI)}, {a: decQuantity(0, 0, DecimalSI), expected: intQuantity(0, 0, DecimalSI)}, {a: decQuantity(10, 0, BinarySI), expected: intQuantity(-10, 0, BinarySI)}, {a: decQuantity(-10, 0, BinarySI), expected: intQuantity(10, 0, BinarySI)}, } for i, test := range tests { a := test.a.DeepCopy() a.Neg() // ensure value is same if a.Cmp(test.expected) != 0 { t.Errorf("[%d] Expected %q, got %q", i, test.expected.String(), a.String()) } } } func TestAdd(t *testing.T) { tests := []struct { a Quantity b Quantity expected Quantity }{ {decQuantity(10, 0, DecimalSI), decQuantity(1, 1, DecimalSI), decQuantity(20, 0, DecimalSI)}, {decQuantity(10, 0, DecimalSI), decQuantity(1, 0, BinarySI), decQuantity(11, 0, DecimalSI)}, {decQuantity(10, 0, BinarySI), decQuantity(1, 0, DecimalSI), decQuantity(11, 0, BinarySI)}, {Quantity{Format: DecimalSI}, decQuantity(50, 0, DecimalSI), decQuantity(50, 0, DecimalSI)}, {decQuantity(50, 0, DecimalSI), Quantity{Format: DecimalSI}, decQuantity(50, 0, DecimalSI)}, {Quantity{Format: DecimalSI}, Quantity{Format: DecimalSI}, decQuantity(0, 0, DecimalSI)}, } for i, test := range tests { test.a.Add(test.b) if test.a.Cmp(test.expected) != 0 { t.Errorf("[%d] Expected %q, got %q", i, test.expected.String(), test.a.String()) } } } func TestAddSubRoundTrip(t *testing.T) { for k := -10; k <= 10; k++ { q := Quantity{Format: DecimalSI} var order []int64 for i := 0; i < 100; i++ { j := rand.Int63() order = append(order, j) q.Add(*NewScaledQuantity(j, Scale(k))) } for _, j := range order { q.Sub(*NewScaledQuantity(j, Scale(k))) } if !q.IsZero() { t.Errorf("addition and subtraction did not cancel: %s", &q) } } } func TestAddSubRoundTripAcrossScales(t *testing.T) { q := Quantity{Format: DecimalSI} var order []int64 for i := 0; i < 100; i++ { j := rand.Int63() order = append(order, j) q.Add(*NewScaledQuantity(j, Scale(j%20-10))) } for _, j := range order { q.Sub(*NewScaledQuantity(j, Scale(j%20-10))) } if !q.IsZero() { t.Errorf("addition and subtraction did not cancel: %s", &q) } } func TestNegateRoundTrip(t *testing.T) { for _, asDec := range []bool{false, true} { for k := -10; k <= 10; k++ { for i := 0; i < 100; i++ { j := rand.Int63() q := *NewScaledQuantity(j, Scale(k)) if asDec { q.AsDec() } b := q.DeepCopy() b.Neg() b.Neg() if b.Cmp(q) != 0 { t.Errorf("double negation did not cancel: %s", &q) } } } } } func TestQuantityAsApproximateFloat64(t *testing.T) { table := []struct { in Quantity out float64 }{ {decQuantity(0, 0, DecimalSI), 0.0}, {decQuantity(0, 0, DecimalExponent), 0.0}, {decQuantity(0, 0, BinarySI), 0.0}, {decQuantity(1, 0, DecimalSI), 1}, {decQuantity(1, 0, DecimalExponent), 1}, {decQuantity(1, 0, BinarySI), 1}, // Binary suffixes {decQuantity(1024, 0, BinarySI), 1024}, {decQuantity(8*1024, 0, BinarySI), 8 * 1024}, {decQuantity(7*1024*1024, 0, BinarySI), 7 * 1024 * 1024}, {decQuantity(7*1024*1024, 1, BinarySI), (7 * 1024 * 1024) * 1024}, {decQuantity(7*1024*1024, 4, BinarySI), (7 * 1024 * 1024) * (1024 * 1024 * 1024 * 1024)}, {decQuantity(7*1024*1024, 8, BinarySI), (7 * 1024 * 1024) * (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)}, {decQuantity(7*1024*1024, -1, BinarySI), (7 * 1024 * 1024) / float64(1024)}, {decQuantity(7*1024*1024, -8, BinarySI), (7 * 1024 * 1024) / float64(1024*1024*1024*1024*1024*1024*1024*1024)}, {decQuantity(1024, 0, DecimalSI), 1024}, {decQuantity(8*1024, 0, DecimalSI), 8 * 1024}, {decQuantity(7*1024*1024, 0, DecimalSI), 7 * 1024 * 1024}, {decQuantity(7*1024*1024, 1, DecimalSI), (7 * 1024 * 1024) * 10}, {decQuantity(7*1024*1024, 4, DecimalSI), (7 * 1024 * 1024) * 10000}, {decQuantity(7*1024*1024, 8, DecimalSI), (7 * 1024 * 1024) * 100000000}, {decQuantity(7*1024*1024, -1, DecimalSI), (7 * 1024 * 1024) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way {decQuantity(7*1024*1024, -8, DecimalSI), (7 * 1024 * 1024) / float64(100000000)}, {decQuantity(1024, 0, DecimalExponent), 1024}, {decQuantity(8*1024, 0, DecimalExponent), 8 * 1024}, {decQuantity(7*1024*1024, 0, DecimalExponent), 7 * 1024 * 1024}, {decQuantity(7*1024*1024, 1, DecimalExponent), (7 * 1024 * 1024) * 10}, {decQuantity(7*1024*1024, 4, DecimalExponent), (7 * 1024 * 1024) * 10000}, {decQuantity(7*1024*1024, 8, DecimalExponent), (7 * 1024 * 1024) * 100000000}, {decQuantity(7*1024*1024, -1, DecimalExponent), (7 * 1024 * 1024) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way {decQuantity(7*1024*1024, -8, DecimalExponent), (7 * 1024 * 1024) / float64(100000000)}, // very large numbers {Quantity{d: maxAllowed, Format: DecimalSI}, math.MaxInt64}, {Quantity{d: maxAllowed, Format: BinarySI}, math.MaxInt64}, {decQuantity(12, 18, DecimalSI), 1.2e19}, // infinities caused due to float64 overflow {decQuantity(12, 500, DecimalSI), math.Inf(0)}, {decQuantity(-12, 500, DecimalSI), math.Inf(-1)}, } for _, item := range table { t.Run(fmt.Sprintf("%s %s", item.in.Format, item.in.String()), func(t *testing.T) { out := item.in.AsApproximateFloat64() if out != item.out { t.Fatalf("expected %v, got %v", item.out, out) } if item.in.d.Dec != nil { if i, ok := item.in.AsInt64(); ok { q := intQuantity(i, 0, item.in.Format) out := q.AsApproximateFloat64() if out != item.out { t.Fatalf("as int quantity: expected %v, got %v", item.out, out) } } } }) } } func benchmarkQuantities() []Quantity { return []Quantity{ intQuantity(1024*1024*1024, 0, BinarySI), intQuantity(1024*1024*1024*1024, 0, BinarySI), intQuantity(1000000, 3, DecimalSI), intQuantity(1000000000, 0, DecimalSI), intQuantity(1, -3, DecimalSI), intQuantity(80, -3, DecimalSI), intQuantity(1080, -3, DecimalSI), intQuantity(0, 0, BinarySI), intQuantity(1, 9, DecimalExponent), intQuantity(1, -9, DecimalSI), intQuantity(1000000, 10, DecimalSI), } } func BenchmarkQuantityString(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() var s string for i := 0; i < b.N; i++ { q := values[i%len(values)] q.s = "" s = q.String() } b.StopTimer() if len(s) == 0 { b.Fatal(s) } } func BenchmarkQuantityStringPrecalc(b *testing.B) { values := benchmarkQuantities() for i := range values { _ = values[i].String() } b.ResetTimer() var s string for i := 0; i < b.N; i++ { q := values[i%len(values)] s = q.String() } b.StopTimer() if len(s) == 0 { b.Fatal(s) } } func BenchmarkQuantityStringBinarySI(b *testing.B) { values := benchmarkQuantities() for i := range values { values[i].Format = BinarySI } b.ResetTimer() var s string for i := 0; i < b.N; i++ { q := values[i%len(values)] q.s = "" s = q.String() } b.StopTimer() if len(s) == 0 { b.Fatal(s) } } func BenchmarkQuantityMarshalJSON(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() for i := 0; i < b.N; i++ { q := values[i%len(values)] q.s = "" if _, err := q.MarshalJSON(); err != nil { b.Fatal(err) } } b.StopTimer() } func BenchmarkQuantityUnmarshalJSON(b *testing.B) { values := benchmarkQuantities() var json [][]byte for _, v := range values { data, _ := v.MarshalJSON() json = append(json, data) } b.ResetTimer() for i := 0; i < b.N; i++ { var q Quantity if err := q.UnmarshalJSON(json[i%len(values)]); err != nil { b.Fatal(err) } } b.StopTimer() } func BenchmarkParseQuantity(b *testing.B) { values := benchmarkQuantities() var strings []string for _, v := range values { strings = append(strings, v.String()) } b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := ParseQuantity(strings[i%len(values)]); err != nil { b.Fatal(err) } } b.StopTimer() } func BenchmarkCanonicalize(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() buffer := make([]byte, 0, 100) for i := 0; i < b.N; i++ { s, _ := values[i%len(values)].CanonicalizeBytes(buffer) if len(s) == 0 { b.Fatal(s) } } b.StopTimer() } func BenchmarkQuantityRoundUp(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() for i := 0; i < b.N; i++ { q := values[i%len(values)] copied := q copied.RoundUp(-3) } b.StopTimer() } func BenchmarkQuantityCopy(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() for i := 0; i < b.N; i++ { values[i%len(values)].DeepCopy() } b.StopTimer() } func BenchmarkQuantityAdd(b *testing.B) { values := benchmarkQuantities() base := &Quantity{} b.ResetTimer() for i := 0; i < b.N; i++ { q := values[i%len(values)] base.d.Dec = nil base.i = int64Amount{value: 100} base.Add(q) } b.StopTimer() } func BenchmarkQuantityCmp(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() for i := 0; i < b.N; i++ { q := values[i%len(values)] if q.Cmp(q) != 0 { b.Fatal(q) } } b.StopTimer() } func BenchmarkQuantityAsApproximateFloat64(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() for i := 0; i < b.N; i++ { q := values[i%len(values)] if q.AsApproximateFloat64() == -1 { b.Fatal(q) } } b.StopTimer() }