// Copyright 2016 The Cockroach 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 apd import ( "bufio" "bytes" "flag" "fmt" "io" "os" "path/filepath" "sort" "strconv" "strings" "sync" "testing" "time" ) const testDir = "testdata" var ( flagFailFast = flag.Bool("fast", false, "stop work after first error; disables parallel testing") flagIgnore = flag.Bool("ignore", false, "print ignore lines on errors") flagNoParallel = flag.Bool("noparallel", false, "disables parallel testing") flagTime = flag.Duration("time", 0, "interval at which to print long-running functions; 0 disables") ) type TestCase struct { Precision int MaxExponent, MinExponent int Rounding string Extended, Clamp bool ID string Operation string Operands []string Result string Conditions []string } func (tc TestCase) HasNull() bool { if tc.Result == "#" { return true } for _, o := range tc.Operands { if o == "#" { return true } } return false } func (tc TestCase) SkipPrecision() bool { switch tc.Operation { case "tosci", "toeng", "apply": return false default: return true } } func ParseDecTest(r io.Reader) ([]TestCase, error) { scanner := bufio.NewScanner(r) tc := TestCase{ Extended: true, } var err error var res []TestCase for scanner.Scan() { text := scanner.Text() // TODO(mjibson): support these test cases if strings.Contains(text, "#") { continue } line := strings.Fields(strings.ToLower(text)) for i, t := range line { if strings.HasPrefix(t, "--") { line = line[:i] break } } if len(line) == 0 { continue } if strings.HasSuffix(line[0], ":") { if len(line) != 2 { return nil, fmt.Errorf("expected 2 tokens, got %q", text) } switch directive := line[0]; directive[:len(directive)-1] { case "precision": tc.Precision, err = strconv.Atoi(line[1]) if err != nil { return nil, err } case "maxexponent": tc.MaxExponent, err = strconv.Atoi(line[1]) if err != nil { return nil, err } case "minexponent": tc.MinExponent, err = strconv.Atoi(line[1]) if err != nil { return nil, err } case "rounding": tc.Rounding = line[1] case "version": // ignore case "extended": tc.Extended = line[1] == "1" case "clamp": tc.Clamp = line[1] == "1" default: return nil, fmt.Errorf("unsupported directive: %s", directive) } } else { if len(line) < 5 { return nil, fmt.Errorf("short test case line: %q", text) } tc.ID = line[0] tc.Operation = line[1] tc.Operands = nil var ops []string line = line[2:] for i, o := range line { if o == "->" { tc.Operands = ops line = line[i+1:] break } o = cleanNumber(o) ops = append(ops, o) } if tc.Operands == nil || len(line) < 1 { return nil, fmt.Errorf("bad test case line: %q", text) } tc.Result = strings.ToUpper(cleanNumber(line[0])) tc.Conditions = line[1:] res = append(res, tc) } } if err := scanner.Err(); err != nil { return nil, err } return res, nil } func cleanNumber(s string) string { if len(s) > 1 && s[0] == '\'' && s[len(s)-1] == '\'' { s = s[1 : len(s)-1] s = strings.Replace(s, `''`, `'`, -1) } else if len(s) > 1 && s[0] == '"' && s[len(s)-1] == '"' { s = s[1 : len(s)-1] s = strings.Replace(s, `""`, `"`, -1) } return s } // Copy ioutil.ReadDir to avoid staticcheck warning on go1.19 and above. Replace // with a call to os.ReadDir when we remove support for go1.15 and below. func ReadDir(dirname string) ([]os.FileInfo, error) { f, err := os.Open(dirname) if err != nil { return nil, err } list, err := f.Readdir(-1) f.Close() if err != nil { return nil, err } sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() }) return list, nil } func TestParseDecTest(t *testing.T) { files, err := ReadDir(testDir) if err != nil { t.Fatal(err) } for _, fi := range files { t.Run(fi.Name(), func(t *testing.T) { f, err := os.Open(filepath.Join(testDir, fi.Name())) if err != nil { t.Fatal(err) } defer f.Close() _, err = ParseDecTest(f) if err != nil { t.Fatal(err) } }) } } var GDAfiles = []string{ "abs", "add", "base", "compare", "comparetotal", "divide", "divideint", "exp", "ln", "log10", "minus", "multiply", "plus", "power", "powersqrt", "quantize", "randoms", "reduce", "remainder", "rounding", "squareroot", "subtract", "tointegral", "tointegralx", // non-GDA tests "cuberoot-apd", } func TestGDA(t *testing.T) { var buf bytes.Buffer fmt.Fprintf(&buf, "%10s%8s%8s%8s%8s%8s%8s\n", "name", "total", "success", "fail", "ignore", "skip", "missing") for _, fname := range GDAfiles { succeed := t.Run(fname, func(t *testing.T) { path, tcs := readGDA(t, fname) gdaTest(t, path, tcs) }) if !succeed && *flagFailFast { break } } } func (tc TestCase) Run(c *Context, done chan error, d, x, y *Decimal) (res Condition, err error) { switch tc.Operation { case "abs": res, err = c.Abs(d, x) case "add": res, err = c.Add(d, x, y) case "compare": res, err = c.Cmp(d, x, y) case "cuberoot": res, err = c.Cbrt(d, x) case "divide": res, err = c.Quo(d, x, y) case "divideint": res, err = c.QuoInteger(d, x, y) case "exp": res, err = c.Exp(d, x) case "ln": res, err = c.Ln(d, x) case "log10": res, err = c.Log10(d, x) case "minus": res, err = c.Neg(d, x) case "multiply": res, err = c.Mul(d, x, y) case "plus": res, err = c.Add(d, x, decimalZero) case "power": res, err = c.Pow(d, x, y) case "quantize": res, err = c.Quantize(d, x, y.Exponent) case "reduce": _, res, err = c.Reduce(d, x) case "remainder": res, err = c.Rem(d, x, y) case "squareroot": res, err = c.Sqrt(d, x) case "subtract": res, err = c.Sub(d, x, y) case "tointegral": res, err = c.RoundToIntegralValue(d, x) case "tointegralx": res, err = c.RoundToIntegralExact(d, x) // Below used only in benchmarks. Tests call it themselves. case "comparetotal": x.CmpTotal(y) case "tosci": _ = x.String() default: done <- fmt.Errorf("unknown operation: %s", tc.Operation) } return } // BenchmarkGDA benchmarks a GDA test. It should not be used without specifying // a sub-benchmark to run. For example: // go test -run XX -bench GDA/squareroot func BenchmarkGDA(b *testing.B) { for _, fname := range GDAfiles { b.Run(fname, func(b *testing.B) { type benchCase struct { tc TestCase ctx *Context ops [2]*Decimal } _, tcs := readGDA(b, fname) bcs := make([]benchCase, 0, len(tcs)) Loop: for _, tc := range tcs { if GDAignore[tc.ID] || tc.Result == "?" || tc.HasNull() { continue } switch tc.Operation { case "apply", "toeng": continue } bc := benchCase{ tc: tc, ctx: tc.Context(b), } for i, o := range tc.Operands { d, _, err := NewFromString(o) if err != nil { continue Loop } bc.ops[i] = d } bcs = append(bcs, bc) } // Translate inputs and outputs to Decimal vectors. op1s := make([]Decimal, len(bcs)) op2s := make([]Decimal, len(bcs)) res := make([]Decimal, b.N*len(bcs)) for i, bc := range bcs { op1s[i].Set(bc.ops[0]) if bc.ops[1] != nil { op2s[i].Set(bc.ops[1]) } } b.ResetTimer() for i := 0; i < b.N; i++ { for j, bc := range bcs { // Ignore errors here because the full tests catch them. _, _ = bc.tc.Run(bc.ctx, nil, &res[i*len(bcs)+j], &op1s[j], &op2s[j]) } } }) } } func readGDA(t testing.TB, name string) (string, []TestCase) { path := filepath.Join(testDir, name+".decTest") f, err := os.Open(path) if err != nil { t.Fatal(err) } defer f.Close() tcs, err := ParseDecTest(f) if err != nil { t.Fatal(err) } return path, tcs } func (tc TestCase) Context(t testing.TB) *Context { rounding := Rounder(tc.Rounding) if _, ok := roundings[rounding]; !ok { t.Fatalf("unsupported rounding mode %s", tc.Rounding) } c := &Context{ Precision: uint32(tc.Precision), MaxExponent: int32(tc.MaxExponent), MinExponent: int32(tc.MinExponent), Rounding: rounding, Traps: 0, } return c } func gdaTest(t *testing.T, path string, tcs []TestCase) { for _, tc := range tcs { tc := tc succeed := t.Run(tc.ID, func(t *testing.T) { if *flagTime > 0 { timeDone := make(chan struct{}, 1) go func() { start := time.Now() for { select { case <-timeDone: return case <-time.After(*flagTime): fmt.Println(tc.ID, "running for", time.Since(start)) } } }() defer func() { timeDone <- struct{}{} }() } defer func() { if t.Failed() { if *flagIgnore { tc.PrintIgnore() } } }() if GDAignore[tc.ID] { t.Skip("ignored") } if tc.HasNull() { t.Skip("has null") } switch tc.Operation { case "toeng", "apply": t.Skip("unsupported") } if !*flagNoParallel && !*flagFailFast { t.Parallel() } // helpful acme address link t.Logf("%s:/^%s ", path, tc.ID) t.Logf("%s %s = %s (%s)", tc.Operation, strings.Join(tc.Operands, " "), tc.Result, strings.Join(tc.Conditions, " ")) t.Logf("prec: %d, round: %s, Emax: %d, Emin: %d", tc.Precision, tc.Rounding, tc.MaxExponent, tc.MinExponent) operands := make([]*Decimal, 2) c := tc.Context(t) var res, opres Condition opctx := c if tc.SkipPrecision() { opctx = opctx.WithPrecision(1000) opctx.MaxExponent = MaxExponent opctx.MinExponent = MinExponent } for i, o := range tc.Operands { d, ores, err := opctx.NewFromString(o) expectError := tc.Result == "NAN" && strings.Join(tc.Conditions, "") == "conversion_syntax" if err != nil { if expectError { // Successfully detected bad syntax. return } switch tc.Operation { case "tosci": // Skip cases with exponents larger than we will parse. if strings.Contains(err.Error(), "value out of range") { return } } testExponentError(t, err) if tc.Result == "?" { return } t.Logf("%v, %v, %v", tc.Result, tc.Conditions, tc.Operation) t.Fatalf("operand %d: %s: %+v", i, o, err) } else if expectError { t.Fatalf("expected error, got %s", d) } operands[i] = d opres |= ores } switch tc.Operation { case "power": tmp := new(Decimal).Abs(operands[1]) // We don't handle power near the max exp limit. if tmp.Cmp(New(MaxExponent, 0)) >= 0 { t.Skip("x ** large y") } if tmp.Cmp(New(int64(c.MaxExponent), 0)) >= 0 { t.Skip("x ** large y") } case "quantize": if operands[1].Form != Finite { t.Skip("quantize requires finite second operand") } } var s string // Fill d with bogus data to make sure all fields are correctly set. d := &Decimal{ Form: -2, Negative: true, Exponent: -6437897, } // Use d1 and d2 to verify that the result can be the same as the first and // second operand. var d1, d2 *Decimal d.Coeff.SetInt64(9221) start := time.Now() defer func() { t.Logf("duration: %s", time.Since(start)) }() done := make(chan error, 1) var err error go func() { switch tc.Operation { case "tosci": s = operands[0].String() // non-extended tests don't retain exponents for 0 if !tc.Extended && operands[0].IsZero() { s = "0" } // Clear d's bogus data. d.Set(operands[0]) // Set d1 to prevent the result-equals-operand check failing. d1 = d case "comparetotal": var c int c = operands[0].CmpTotal(operands[1]) d.SetInt64(int64(c)) default: var wg sync.WaitGroup wg.Add(2) // Check that the result is correct even if it is either argument. Use some // go routines since we are running tc.Run three times. go func() { d1 = new(Decimal).Set(operands[0]) tc.Run(c, done, d1, d1, operands[1]) wg.Done() }() go func() { if operands[1] != nil { d2 = new(Decimal).Set(operands[1]) tc.Run(c, done, d2, operands[0], d2) } wg.Done() }() res, err = tc.Run(c, done, d, operands[0], operands[1]) wg.Wait() } done <- nil }() select { case err := <-done: if err != nil { t.Fatal(err) } case <-time.After(time.Second * 20): t.Fatalf("timeout") } if d.Coeff.Sign() < 0 { t.Fatalf("negative coeff: %s", d.Coeff.String()) } // Make sure the bogus Form above got cleared. if d.Form < 0 { t.Fatalf("unexpected form: %#v", d) } // Verify the operands didn't change. for i, o := range tc.Operands { v := newDecimal(t, opctx, o) if v.CmpTotal(operands[i]) != 0 { t.Fatalf("operand %d changed from %s to %s", i, o, operands[i]) } } // Verify the result-equals-operand worked correctly. if d1 != nil && d.CmpTotal(d1) != 0 { t.Errorf("first operand as result mismatch: got %s, expected %s", d1, d) } if d2 != nil && d.CmpTotal(d2) != 0 { t.Errorf("second operand as result mismatch: got %s, expected %s", d2, d) } if !GDAignoreFlags[tc.ID] { var rcond Condition for _, cond := range tc.Conditions { switch cond { case "underflow": rcond |= Underflow case "inexact": rcond |= Inexact case "overflow": rcond |= Overflow case "subnormal": rcond |= Subnormal case "division_undefined": rcond |= DivisionUndefined case "division_by_zero": rcond |= DivisionByZero case "division_impossible": rcond |= DivisionImpossible case "invalid_operation": rcond |= InvalidOperation case "rounded": rcond |= Rounded case "clamped": rcond |= Clamped case "invalid_context": // ignore default: t.Fatalf("unknown condition: %s", cond) } } switch tc.Operation { case "tosci": // We only care about the operand flags for the string conversion operations. res |= opres } t.Logf("want flags (%d): %s", rcond, rcond) t.Logf("have flags (%d): %s", res, res) // TODO(mjibson): after upscaling, operations need to remove the 0s added // after the operation is done. Since this isn't happening, things are being // rounded when they shouldn't because the coefficient has so many trailing 0s. // Manually remove Rounded flag from context until the TODO is fixed. res &= ^Rounded rcond &= ^Rounded switch tc.Operation { case "log10": // TODO(mjibson): Under certain conditions these are exact, but we don't // correctly mark them. Ignore these flags for now. // squareroot sometimes marks things exact when GDA says they should be // inexact. rcond &= ^Inexact res &= ^Inexact } // Don't worry about these flags; they are handled by GoError. res &= ^SystemOverflow res &= ^SystemUnderflow if (res.Overflow() || res.Underflow()) && (strings.HasPrefix(tc.ID, "rpow") || strings.HasPrefix(tc.ID, "powr")) { t.Skip("overflow") } // Ignore Clamped on error. if tc.Result == "?" { rcond &= ^Clamped res &= ^Clamped } if rcond != res { if tc.Operation == "power" && (res.Overflow() || res.Underflow()) { t.Skip("power overflow") } t.Logf("got: %s (%#v)", d, d) t.Logf("error: %+v", err) t.Errorf("expected flags %q (%d); got flags %q (%d)", rcond, rcond, res, res) } } if tc.Result == "?" { if err != nil { return } t.Fatalf("expected error, got %s", d) } if err != nil { testExponentError(t, err) if tc.Operation == "power" && (res.Overflow() || res.Underflow()) { t.Skip("power overflow") } t.Fatalf("%+v", err) } switch tc.Operation { case "tosci", "toeng": if strings.HasPrefix(tc.Result, "-NAN") { tc.Result = "-NaN" } if strings.HasPrefix(tc.Result, "-SNAN") { tc.Result = "-sNaN" } if strings.HasPrefix(tc.Result, "NAN") { tc.Result = "NaN" } if strings.HasPrefix(tc.Result, "SNAN") { tc.Result = "sNaN" } expected := tc.Result // Adjust 0E- or -0E- tests to match PostgreSQL behavior. // See: https://github.com/cockroachdb/cockroach/issues/102217. if pos, neg := strings.HasPrefix(expected, "0E-"), strings.HasPrefix(expected, "-0E-"); pos || neg { startIdx := 3 if neg { startIdx = 4 } p, err := strconv.ParseInt(expected[startIdx:], 10, 64) if err != nil { t.Fatalf("unexpected error converting int: %v", err) } if p <= -lowestZeroNegativeCoefficientCockroach { expected = "" if neg { expected = "-" } expected += "0." + strings.Repeat("0", int(p)) } } if !strings.EqualFold(s, expected) { t.Fatalf("expected %s, got %s", expected, s) } return } r := newDecimal(t, testCtx, tc.Result) var equal bool if d.Form == Finite { // Don't worry about trailing zeros being inequal in CmpTotal. equal = d.Cmp(r) == 0 && d.Negative == r.Negative } else { equal = d.CmpTotal(r) == 0 } if !equal { t.Logf("want: %s", tc.Result) t.Logf("got: %s (%#v)", d, d) // Some operations allow 1ulp of error in tests. switch tc.Operation { case "exp", "ln", "log10", "power": nc := c.WithPrecision(0) nc.Sub(d, d, r) if d.Coeff.Cmp(bigOne) == 0 { t.Logf("pass: within 1ulp: %s, %s", d, r) return } } t.Fatalf("unexpected result") } else { t.Logf("got: %s (%#v)", d, d) } }) if !succeed { if *flagFailFast { break } } } } func (tc TestCase) PrintIgnore() { fmt.Printf(" \"%s\": true,\n", tc.ID) } var GDAignore = map[string]bool{ // Invalid context "expx901": true, "expx902": true, "expx903": true, "expx905": true, "lnx901": true, "lnx902": true, "lnx903": true, "lnx905": true, "logx901": true, "logx902": true, "logx903": true, "logx905": true, "powx4001": true, "powx4002": true, "powx4003": true, "powx4005": true, // NaN payloads with weird digits "basx725": true, "basx745": true, // NaN payloads "cotx970": true, "cotx973": true, "cotx974": true, "cotx977": true, "cotx980": true, "cotx983": true, "cotx984": true, "cotx987": true, "cotx994": true, // too large exponents, supposed to fail anyway "quax525": true, "quax531": true, "quax805": true, "quax806": true, "quax807": true, "quax808": true, "quax809": true, "quax810": true, "quax811": true, "quax812": true, "quax813": true, "quax814": true, "quax815": true, "quax816": true, "quax817": true, "quax818": true, "quax819": true, "quax820": true, "quax821": true, "quax822": true, "quax861": true, "quax862": true, "quax866": true, // TODO(mjibson): fix tests below // overflows to infinity "powx4125": true, "powx4145": true, // exceeds system overflow "expx291": true, "expx292": true, "expx293": true, "expx294": true, "expx295": true, "expx296": true, // inexact zeros "addx1633": true, "addx1634": true, "addx1638": true, "addx61633": true, "addx61634": true, "addx61638": true, // should be -0E-398, got -1E-398 "addx1613": true, "addx1614": true, "addx1618": true, "addx61613": true, "addx61614": true, "addx61618": true, // extreme input range, but should work "sqtx8636": true, "sqtx8644": true, "sqtx8646": true, "sqtx8647": true, "sqtx8648": true, "sqtx8650": true, "sqtx8651": true, } var GDAignoreFlags = map[string]bool{ // unflagged clamped "sqtx9024": true, "sqtx9025": true, "sqtx9026": true, "sqtx9027": true, "sqtx9038": true, "sqtx9039": true, "sqtx9040": true, "sqtx9045": true, }