internal/strconv: add testbase tests

Add ability to test against inputs chosen by from the stress
tests computed by Vern Paxson's testbase program.

Checked that 'go test -testbase' passes.

Change-Id: I81057e55df6cd369b40ce623a59884e6ead0ed76
Reviewed-on: https://go-review.googlesource.com/c/go/+/719620
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Russ Cox <rsc@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Russ Cox 2025-11-11 05:47:14 -08:00 committed by Gopher Robot
parent 6954be0baa
commit 89f6dba7e6
6 changed files with 2174 additions and 16 deletions

View file

@ -5,10 +5,13 @@
package strconv_test
import (
"bufio"
_ "embed"
"flag"
"fmt"
"internal/strconv"
. "internal/strconv"
"io"
"net/http"
"os"
"strings"
"testing"
)
@ -25,15 +28,15 @@ func pow2(i int) float64 {
return pow2(i/2) * pow2(i-i/2)
}
// Wrapper around strconv.ParseFloat(x, 64). Handles dddddp+ddd (binary exponent)
// itself, passes the rest on to strconv.ParseFloat.
// Wrapper around ParseFloat(x, 64). Handles dddddp+ddd (binary exponent)
// itself, passes the rest on to ParseFloat.
func myatof64(s string) (f float64, ok bool) {
if mant, exp, ok := strings.Cut(s, "p"); ok {
n, err := strconv.ParseInt(mant, 10, 64)
n, err := ParseInt(mant, 10, 64)
if err != nil {
return 0, false
}
e, err1 := strconv.Atoi(exp)
e, err1 := Atoi(exp)
if err1 != nil {
println("bad e", exp)
return 0, false
@ -61,7 +64,7 @@ func myatof64(s string) (f float64, ok bool) {
}
return v * pow2(e), true
}
f1, err := strconv.ParseFloat(s, 64)
f1, err := ParseFloat(s, 64)
if err != nil {
return 0, false
}
@ -72,19 +75,19 @@ func myatof64(s string) (f float64, ok bool) {
// itself, passes the rest on to strconv.ParseFloat.
func myatof32(s string) (f float32, ok bool) {
if mant, exp, ok := strings.Cut(s, "p"); ok {
n, err := strconv.Atoi(mant)
n, err := Atoi(mant)
if err != nil {
println("bad n", mant)
return 0, false
}
e, err1 := strconv.Atoi(exp)
e, err1 := Atoi(exp)
if err1 != nil {
println("bad p", exp)
return 0, false
}
return float32(float64(n) * pow2(e)), true
}
f64, err1 := strconv.ParseFloat(s, 32)
f64, err1 := ParseFloat(s, 32)
f1 := float32(f64)
if err1 != nil {
return 0, false
@ -96,9 +99,9 @@ func myatof32(s string) (f float32, ok bool) {
var testfp string
func TestFp(t *testing.T) {
s := bufio.NewScanner(strings.NewReader(testfp))
for lineno := 1; s.Scan(); lineno++ {
line := s.Text()
lineno := 0
for line := range strings.Lines(testfp) {
lineno++
line, _, _ = strings.Cut(line, "#")
line = strings.TrimSpace(line)
if line == "" {
@ -133,7 +136,128 @@ func TestFp(t *testing.T) {
t.Errorf("testdata/testfp.txt:%d: %s %s %s %s: have %s want %s", lineno, a[0], a[1], a[2], a[3], s, a[3])
}
}
if s.Err() != nil {
t.Fatal("testfp: read testdata/testfp.txt: ", s.Err())
}
// The -testbase flag runs the full testbase input set instead of the
// random sample in testdata/*1k.txt. See testdata/README for details.
var testbase = flag.Bool("testbase", false, "download and test full testbase testdata")
// testbaseURL is the URL for downloading the full testbase testdata.
// There is also a copy on "https://swtch.com/testbase/".
var testbaseURL = "https://gist.githubusercontent.com/rsc/606b378b0bf95c24a6fd6cef99e262e1/raw/128a03890e536bdf403e6cc768b0737405c6734d/"
//go:embed testdata/atof1k.txt
var atof1ktxt string
//go:embed testdata/ftoa1k.txt
var ftoa1ktxt string
// openTestbase opens the named testbase data file.
// By default it opens testdata/name1k.txt,
// but if the -testbase flag has been set,
// then it opens the full testdata/name.txt,
// downloading that file if necessary.
func openTestbase(t *testing.T, name string) (file, data string) {
if !*testbase {
switch name {
case "atof":
return "testdata/atof1k.txt", atof1ktxt
case "ftoa":
return "testdata/ftoa1k.txt", ftoa1ktxt
}
t.Fatalf("unknown file %s", name)
}
// Use cached copy if present.
file = "testdata/" + name + ".txt"
if data, err := os.ReadFile(file); err == nil {
return file, string(data)
}
// Download copy.
url := testbaseURL + name + ".txt"
resp, err := http.Get(url)
if err != nil {
t.Fatalf("%s: %s", url, err)
}
if resp.StatusCode != 200 {
t.Fatalf("%s: %s", url, resp.Status)
}
bytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
t.Fatalf("%s: %s", url, err)
}
if err := os.WriteFile(file, bytes, 0666); err != nil {
t.Fatal(err)
}
return file, string(bytes)
}
func TestParseFloatTestdata(t *testing.T) {
// Test testbase inputs, optimized against not.
name, data := openTestbase(t, "atof")
fail := 0
lineno := 0
for line := range strings.Lines(data) {
lineno++
s := strings.TrimSpace(line)
if strings.HasPrefix(s, "#") || s == "" {
continue
}
SetOptimize(false)
want, err1 := ParseFloat(s, 64)
SetOptimize(true)
have, err2 := ParseFloat(s, 64)
if err1 != nil {
// Error in test data; should not happen.
t.Errorf("%s:%d: ParseFloat(%#q): %v", name, lineno, s, err1)
continue
}
if err2 != nil {
t.Errorf("ParseFloat(%#q): %v", s, err2)
if fail++; fail > 100 {
t.Fatalf("too many failures")
}
continue
}
if have != want {
t.Errorf("ParseFloat(%#q) = %#x, want %#x", s, have, want)
if fail++; fail > 100 {
t.Fatalf("too many failures")
}
}
}
}
func TestFormatFloatTestdata(t *testing.T) {
// Test testbase inputs, optimized against not.
name, data := openTestbase(t, "ftoa")
fail := 0
lineno := 0
for line := range strings.Lines(data) {
lineno++
s := strings.TrimSpace(line)
if strings.HasPrefix(s, "#") || s == "" {
continue
}
f, err := ParseFloat(s, 64)
if err != nil {
// Error in test data; should not happen.
t.Errorf("%s:%d: ParseFloat(%#q): %v", name, lineno, s, err)
continue
}
for i := range 19 {
SetOptimize(false)
want := FormatFloat(f, 'e', i, 64)
SetOptimize(true)
have := FormatFloat(f, 'e', i, 64)
if have != want {
t.Errorf("FormatFloat(%#x, 'e', %d) = %s, want %s", f, i, have, want)
if fail++; fail > 100 {
t.Fatalf("too many failures")
}
}
}
}
}

View file

@ -206,10 +206,17 @@ var ftoatests = []ftoaTest{
{8393378656576888. * (1 << 1), 'e', 15, "1.678675731315378e+16"},
{8738676561280626. * (1 << 4), 'e', 16, "1.3981882498049002e+17"},
{8291032395191335. / (1 << 30), 'e', 5, "7.72163e+06"},
{8880392441509914. / (1 << 80), 'e', 16, "7.3456884594794477e-09"},
// Exercise divisiblePow5 case in fixedFtoa
{2384185791015625. * (1 << 12), 'e', 5, "9.76562e+18"},
{2384185791015625. * (1 << 13), 'e', 5, "1.95312e+19"},
// Exercise potential mistakes in fixedFtoa.
// Found by introducing mistakes and running 'go test -testbase'.
{0x1.000000000005p+71, 'e', 16, "2.3611832414348645e+21"},
{0x1.0000p-27, 'e', 17, "7.45058059692382812e-09"},
{0x1.0000p-41, 'e', 17, "4.54747350886464119e-13"},
}
func TestFtoa(t *testing.T) {

27
src/internal/strconv/testdata/README vendored Normal file
View file

@ -0,0 +1,27 @@
testfp.txt contains conversion tests from Vern Paxson's paper
“A Program for Testing IEEE Decimal-Binary Conversion”
https://www.icir.org/vern/papers/testbase-report.pdf
That paper from 1991 describes a tester called 'testbase',
written in non-64-bit-safe pre-ANSI C.
As of 2025, it is still available at ftp://ftp.ee.lbl.gov/testbase.tar.Z.
The files
https://swtch.com/testbase/atof.txt
https://swtch.com/testbase/ftoa.txt
are the test inputs that testbase generates and checks,
logged during an actual run, totaling about 10 MB.
The files atof1k.txt and ftoa1k.txt in this directory each contain
1000 random samples of the full trace. They are used during
'go test internal/strconv'.
Running 'go test internal/strconv -testbase' downloads the
complete files into this directory as atof.txt and ftoa.txt and
then uses those instead of the sampled versions.
The complete tests take about 10 seconds on a Macbook Pro.
Backup copies of the files are also posted at
https://gist.github.com/rsc/606b378b0bf95c24a6fd6cef99e262e1.

1000
src/internal/strconv/testdata/atof1k.txt vendored Normal file

File diff suppressed because it is too large Load diff

1000
src/internal/strconv/testdata/ftoa1k.txt vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@
# Difficult boundary cases, derived from tables given in
# Vern Paxson, A Program for Testing IEEE Decimal-Binary Conversion
# ftp://ftp.ee.lbl.gov/testbase-report.ps.Z
# https://www.icir.org/vern/papers/testbase-report.pdf
# Table 1: Stress Inputs for Conversion to 53-bit Binary, < 1/2 ULP
float64 %b 5e+125 6653062250012735p+365