blob: 74a0711094a87671cbfd5e8d7ea1f1526115a6db [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package benchfmt
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
func parseAll(t *testing.T, data string, setup ...func(r *Reader, sr io.Reader)) ([]Record, *Reader) {
sr := strings.NewReader(data)
r := NewReader(sr, "test")
for _, f := range setup {
f(r, sr)
}
var out []Record
for r.Scan() {
switch rec := r.Result(); rec := rec.(type) {
case *Result:
res := rec.Clone()
// Wipe position information for comparisons.
res.fileName = ""
res.line = 0
out = append(out, res)
case *SyntaxError, *UnitMetadata:
out = append(out, rec)
default:
t.Fatalf("unexpected result type %T", rec)
}
}
if err := r.Err(); err != nil {
t.Fatal("parsing failed: ", err)
}
return out, r
}
func printRecord(w io.Writer, r Record) {
switch r := r.(type) {
case *Result:
for _, fc := range r.Config {
fmt.Fprintf(w, "{%s: %s} ", fc.Key, fc.Value)
}
fmt.Fprintf(w, "%s %d", r.Name.Full(), r.Iters)
for _, val := range r.Values {
fmt.Fprintf(w, " %v %s", val.Value, val.Unit)
}
fmt.Fprintf(w, "\n")
case *UnitMetadata:
fmt.Fprintf(w, "Unit: %+v\n", r)
case *SyntaxError:
fmt.Fprintf(w, "SyntaxError: %s\n", r)
default:
panic(fmt.Sprintf("unknown record type %T", r))
}
}
type resultBuilder struct {
res *Result
}
func r(fullName string, iters int) *resultBuilder {
return &resultBuilder{
&Result{
Config: []Config{},
Name: Name(fullName),
Iters: iters,
},
}
}
func (b *resultBuilder) config(keyVals ...string) *resultBuilder {
for i := 0; i < len(keyVals); i += 2 {
key, val := keyVals[i], keyVals[i+1]
file := true
if val[0] == '*' {
file = false
val = val[1:]
}
b.res.Config = append(b.res.Config, Config{key, []byte(val), file})
}
return b
}
func (b *resultBuilder) v(value float64, unit string) *resultBuilder {
var v Value
if unit == "ns/op" {
v = Value{Value: value * 1e-9, Unit: "sec/op", OrigValue: value, OrigUnit: unit}
} else {
v = Value{Value: value, Unit: unit}
}
b.res.Values = append(b.res.Values, v)
return b
}
func compareRecords(t *testing.T, got, want []Record) {
t.Helper()
var diff bytes.Buffer
for i := 0; i < len(got) || i < len(want); i++ {
if i >= len(got) {
fmt.Fprintf(&diff, "[%d] got: none, want:\n", i)
printRecord(&diff, want[i])
} else if i >= len(want) {
fmt.Fprintf(&diff, "[%d] want: none, got:\n", i)
printRecord(&diff, got[i])
} else if !reflect.DeepEqual(got[i], want[i]) {
fmt.Fprintf(&diff, "[%d] got:\n", i)
printRecord(&diff, got[i])
fmt.Fprintf(&diff, "[%d] want:\n", i)
printRecord(&diff, want[i])
}
}
if diff.Len() != 0 {
t.Error(diff.String())
}
}
func TestReader(t *testing.T) {
type testCase struct {
name, input string
want []Record
}
for _, test := range []testCase{
{
"basic",
`key: value
BenchmarkOne 100 1 ns/op 2 B/op
BenchmarkTwo 300 4.5 ns/op
`,
[]Record{
r("One", 100).
config("key", "value").
v(1, "ns/op").v(2, "B/op").res,
r("Two", 300).
config("key", "value").
v(4.5, "ns/op").res,
},
},
{
"weird",
`
BenchmarkSpaces 1 1 ns/op
BenchmarkHugeVal 1 9999999999999999999999999999999 ns/op
BenchmarkEmSpace  1  1  ns/op
`,
[]Record{
r("Spaces", 1).
v(1, "ns/op").res,
r("HugeVal", 1).
v(9999999999999999999999999999999, "ns/op").res,
r("EmSpace", 1).
v(1, "ns/op").res,
},
},
{
"basic file keys",
`key1: value
: not a key
ab:not a key
a b: also not a key
key2: value
BenchmarkOne 100 1 ns/op
`,
[]Record{
r("One", 100).
config("key1", "value", "key2", "value").
v(1, "ns/op").res,
},
},
{
"bad lines",
`not a benchmark
BenchmarkTailingSpaceNoIter
BenchmarkBadIter abc
BenchmarkHugeIter 9999999999999999999999999999999
BenchmarkMissingVal 100
BenchmarkBadVal 100 abc
BenchmarkMissingUnit 100 1
BenchmarkMissingUnit2 100 1 ns/op 2
also not a benchmark
Unit
Unit ns/op blah
Unit ns/op a=1
Unit ns/op a=2
`,
[]Record{
&SyntaxError{"test", 2, "missing iteration count"},
&SyntaxError{"test", 3, "parsing iteration count: invalid syntax"},
&SyntaxError{"test", 4, "parsing iteration count: value out of range"},
&SyntaxError{"test", 5, "missing measurements"},
&SyntaxError{"test", 6, "parsing measurement: invalid syntax"},
&SyntaxError{"test", 7, "missing units"},
&SyntaxError{"test", 8, "missing units"},
&SyntaxError{"test", 10, "missing unit"},
&SyntaxError{"test", 11, "expected key=value"},
&UnitMetadata{UnitMetadataKey{"sec/op", "a"}, "ns/op", "1", "test", 12},
&SyntaxError{"test", 13, "metadata a of unit ns/op already set to 1"},
},
},
{
"remove existing label",
`key: value
key:
BenchmarkOne 100 1 ns/op
`,
[]Record{
r("One", 100).
v(1, "ns/op").res,
},
},
{
"overwrite exiting label",
`key1: first
key2: second
key1: third
BenchmarkOne 100 1 ns/op
`,
[]Record{
r("One", 100).
config("key1", "third", "key2", "second").
v(1, "ns/op").res,
},
},
{
"unit metadata",
`Unit ns/op a=1 b=2
Unit ns/op c=3 error d=4
# Repeated unit should report nothing
Unit ns/op d=4
# Starts like a unit line but actually isn't
Unitx
BenchmarkOne 100 1 ns/op
`,
[]Record{
&UnitMetadata{UnitMetadataKey{"sec/op", "a"}, "ns/op", "1", "test", 1},
&UnitMetadata{UnitMetadataKey{"sec/op", "b"}, "ns/op", "2", "test", 1},
&UnitMetadata{UnitMetadataKey{"sec/op", "c"}, "ns/op", "3", "test", 2},
&SyntaxError{"test", 2, "expected key=value"},
&UnitMetadata{UnitMetadataKey{"sec/op", "d"}, "ns/op", "4", "test", 2},
r("One", 100).
v(1, "ns/op").res,
},
},
{
// go test -v prints just the benchmark name
// on a line when starting each benchmark.
// Make sure we ignore it.
"verbose",
`BenchmarkOne
BenchmarkOne 100 1 ns/op
`,
[]Record{
r("One", 100).
v(1, "ns/op").res,
},
},
} {
t.Run(test.name, func(t *testing.T) {
got, _ := parseAll(t, test.input)
compareRecords(t, got, test.want)
})
}
}
func TestReaderInternalConfig(t *testing.T) {
got, _ := parseAll(t, `
# Test initial internal config
Benchmark1 100 1 ns/op
# Overwrite internal config with file config
key1: file1
key3: file3
Benchmark2 100 1 ns/op
# Delete internal config, check that file config is right
key2:
Benchmark3 100 1 ns/op
`, func(r *Reader, sr io.Reader) {
r.Reset(sr, "test", "key1", "internal1", "key2", "internal2")
})
want := []Record{
r("1", 100).v(1, "ns/op").config("key1", "*internal1", "key2", "*internal2").res,
r("2", 100).v(1, "ns/op").config("key1", "file1", "key2", "*internal2", "key3", "file3").res,
r("3", 100).v(1, "ns/op").config("key1", "file1", "key3", "file3").res,
}
compareRecords(t, got, want)
}
func BenchmarkReader(b *testing.B) {
path := "testdata/bent"
fileInfos, err := os.ReadDir(path)
if err != nil {
b.Fatal("reading test data directory: ", err)
}
var files []*os.File
for _, info := range fileInfos {
f, err := os.Open(filepath.Join(path, info.Name()))
if err != nil {
b.Fatal(err)
}
defer f.Close()
files = append(files, f)
}
b.ResetTimer()
start := time.Now()
var n int
for i := 0; i < b.N; i++ {
r := new(Reader)
for _, f := range files {
if _, err := f.Seek(0, 0); err != nil {
b.Fatal("seeking to 0: ", err)
}
r.Reset(f, f.Name())
for r.Scan() {
n++
if err, ok := r.Result().(error); ok {
b.Fatal("malformed record: ", err)
}
}
if err := r.Err(); err != nil {
b.Fatal(err)
}
}
}
dur := time.Since(start)
b.Logf("read %d records", n)
b.StopTimer()
b.ReportMetric(float64(n/b.N), "records/op")
b.ReportMetric(float64(n)*float64(time.Second)/float64(dur), "records/sec")
}