blob: b827c4d621b93966b7a959bfa70f5307963d3480 [file] [log] [blame]
// Copyright 2009 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 expvar
import (
"bytes"
"crypto/sha1"
"encoding/json"
"fmt"
"net"
"net/http/httptest"
"reflect"
"runtime"
"strconv"
"sync"
"sync/atomic"
"testing"
)
// RemoveAll removes all exported variables.
// This is for tests only.
func RemoveAll() {
vars.keysMu.Lock()
defer vars.keysMu.Unlock()
for _, k := range vars.keys {
vars.m.Delete(k)
}
vars.keys = nil
}
func TestNil(t *testing.T) {
RemoveAll()
val := Get("missing")
if val != nil {
t.Errorf("got %v, want nil", val)
}
}
func TestInt(t *testing.T) {
RemoveAll()
reqs := NewInt("requests")
if i := reqs.Value(); i != 0 {
t.Errorf("reqs.Value() = %v, want 0", i)
}
if reqs != Get("requests").(*Int) {
t.Errorf("Get() failed.")
}
reqs.Add(1)
reqs.Add(3)
if i := reqs.Value(); i != 4 {
t.Errorf("reqs.Value() = %v, want 4", i)
}
if s := reqs.String(); s != "4" {
t.Errorf("reqs.String() = %q, want \"4\"", s)
}
reqs.Set(-2)
if i := reqs.Value(); i != -2 {
t.Errorf("reqs.Value() = %v, want -2", i)
}
}
func BenchmarkIntAdd(b *testing.B) {
var v Int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.Add(1)
}
})
}
func BenchmarkIntSet(b *testing.B) {
var v Int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.Set(1)
}
})
}
func TestFloat(t *testing.T) {
RemoveAll()
reqs := NewFloat("requests-float")
if reqs.f.Load() != 0.0 {
t.Errorf("reqs.f = %v, want 0", reqs.f.Load())
}
if reqs != Get("requests-float").(*Float) {
t.Errorf("Get() failed.")
}
reqs.Add(1.5)
reqs.Add(1.25)
if v := reqs.Value(); v != 2.75 {
t.Errorf("reqs.Value() = %v, want 2.75", v)
}
if s := reqs.String(); s != "2.75" {
t.Errorf("reqs.String() = %q, want \"4.64\"", s)
}
reqs.Add(-2)
if v := reqs.Value(); v != 0.75 {
t.Errorf("reqs.Value() = %v, want 0.75", v)
}
}
func BenchmarkFloatAdd(b *testing.B) {
var f Float
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
f.Add(1.0)
}
})
}
func BenchmarkFloatSet(b *testing.B) {
var f Float
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
f.Set(1.0)
}
})
}
func TestString(t *testing.T) {
RemoveAll()
name := NewString("my-name")
if s := name.Value(); s != "" {
t.Errorf(`NewString("my-name").Value() = %q, want ""`, s)
}
name.Set("Mike")
if s, want := name.String(), `"Mike"`; s != want {
t.Errorf(`after name.Set("Mike"), name.String() = %q, want %q`, s, want)
}
if s, want := name.Value(), "Mike"; s != want {
t.Errorf(`after name.Set("Mike"), name.Value() = %q, want %q`, s, want)
}
// Make sure we produce safe JSON output.
name.Set("<")
if s, want := name.String(), "\"\\u003c\""; s != want {
t.Errorf(`after name.Set("<"), name.String() = %q, want %q`, s, want)
}
}
func BenchmarkStringSet(b *testing.B) {
var s String
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
s.Set("red")
}
})
}
func TestMapInit(t *testing.T) {
RemoveAll()
colors := NewMap("bike-shed-colors")
colors.Add("red", 1)
colors.Add("blue", 1)
colors.Add("chartreuse", 1)
n := 0
colors.Do(func(KeyValue) { n++ })
if n != 3 {
t.Errorf("after three Add calls with distinct keys, Do should invoke f 3 times; got %v", n)
}
colors.Init()
n = 0
colors.Do(func(KeyValue) { n++ })
if n != 0 {
t.Errorf("after Init, Do should invoke f 0 times; got %v", n)
}
}
func TestMapDelete(t *testing.T) {
RemoveAll()
colors := NewMap("bike-shed-colors")
colors.Add("red", 1)
colors.Add("red", 2)
colors.Add("blue", 4)
n := 0
colors.Do(func(KeyValue) { n++ })
if n != 2 {
t.Errorf("after two Add calls with distinct keys, Do should invoke f 2 times; got %v", n)
}
colors.Delete("red")
n = 0
colors.Do(func(KeyValue) { n++ })
if n != 1 {
t.Errorf("removed red, Do should invoke f 1 times; got %v", n)
}
colors.Delete("notfound")
n = 0
colors.Do(func(KeyValue) { n++ })
if n != 1 {
t.Errorf("attempted to remove notfound, Do should invoke f 1 times; got %v", n)
}
colors.Delete("blue")
colors.Delete("blue")
n = 0
colors.Do(func(KeyValue) { n++ })
if n != 0 {
t.Errorf("all keys removed, Do should invoke f 0 times; got %v", n)
}
}
func TestMapCounter(t *testing.T) {
RemoveAll()
colors := NewMap("bike-shed-colors")
colors.Add("red", 1)
colors.Add("red", 2)
colors.Add("blue", 4)
colors.AddFloat(`green "midori"`, 4.125)
if x := colors.Get("red").(*Int).Value(); x != 3 {
t.Errorf("colors.m[\"red\"] = %v, want 3", x)
}
if x := colors.Get("blue").(*Int).Value(); x != 4 {
t.Errorf("colors.m[\"blue\"] = %v, want 4", x)
}
if x := colors.Get(`green "midori"`).(*Float).Value(); x != 4.125 {
t.Errorf("colors.m[`green \"midori\"] = %v, want 4.125", x)
}
// colors.String() should be '{"red":3, "blue":4}',
// though the order of red and blue could vary.
s := colors.String()
var j any
err := json.Unmarshal([]byte(s), &j)
if err != nil {
t.Errorf("colors.String() isn't valid JSON: %v", err)
}
m, ok := j.(map[string]any)
if !ok {
t.Error("colors.String() didn't produce a map.")
}
red := m["red"]
x, ok := red.(float64)
if !ok {
t.Error("red.Kind() is not a number.")
}
if x != 3 {
t.Errorf("red = %v, want 3", x)
}
}
func TestMapNil(t *testing.T) {
RemoveAll()
const key = "key"
m := NewMap("issue527719")
m.Set(key, nil)
s := m.String()
var j any
if err := json.Unmarshal([]byte(s), &j); err != nil {
t.Fatalf("m.String() == %q isn't valid JSON: %v", s, err)
}
m2, ok := j.(map[string]any)
if !ok {
t.Fatalf("m.String() produced %T, wanted a map", j)
}
v, ok := m2[key]
if !ok {
t.Fatalf("missing %q in %v", key, m2)
}
if v != nil {
t.Fatalf("m[%q] = %v, want nil", key, v)
}
}
func BenchmarkMapSet(b *testing.B) {
m := new(Map).Init()
v := new(Int)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Set("red", v)
}
})
}
func BenchmarkMapSetDifferent(b *testing.B) {
procKeys := make([][]string, runtime.GOMAXPROCS(0))
for i := range procKeys {
keys := make([]string, 4)
for j := range keys {
keys[j] = fmt.Sprint(i, j)
}
procKeys[i] = keys
}
m := new(Map).Init()
v := new(Int)
b.ResetTimer()
var n int32
b.RunParallel(func(pb *testing.PB) {
i := int(atomic.AddInt32(&n, 1)-1) % len(procKeys)
keys := procKeys[i]
for pb.Next() {
for _, k := range keys {
m.Set(k, v)
}
}
})
}
// BenchmarkMapSetDifferentRandom simulates such a case where the concerned
// keys of Map.Set are generated dynamically and as a result insertion is
// out of order and the number of the keys may be large.
func BenchmarkMapSetDifferentRandom(b *testing.B) {
keys := make([]string, 100)
for i := range keys {
keys[i] = fmt.Sprintf("%x", sha1.Sum([]byte(fmt.Sprint(i))))
}
v := new(Int)
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := new(Map).Init()
for _, k := range keys {
m.Set(k, v)
}
}
}
func BenchmarkMapSetString(b *testing.B) {
m := new(Map).Init()
v := new(String)
v.Set("Hello, ï €!")
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Set("red", v)
}
})
}
func BenchmarkMapAddSame(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m := new(Map).Init()
m.Add("red", 1)
m.Add("red", 1)
m.Add("red", 1)
m.Add("red", 1)
}
})
}
func BenchmarkMapAddDifferent(b *testing.B) {
procKeys := make([][]string, runtime.GOMAXPROCS(0))
for i := range procKeys {
keys := make([]string, 4)
for j := range keys {
keys[j] = fmt.Sprint(i, j)
}
procKeys[i] = keys
}
b.ResetTimer()
var n int32
b.RunParallel(func(pb *testing.PB) {
i := int(atomic.AddInt32(&n, 1)-1) % len(procKeys)
keys := procKeys[i]
for pb.Next() {
m := new(Map).Init()
for _, k := range keys {
m.Add(k, 1)
}
}
})
}
// BenchmarkMapAddDifferentRandom simulates such a case where that the concerned
// keys of Map.Add are generated dynamically and as a result insertion is out of
// order and the number of the keys may be large.
func BenchmarkMapAddDifferentRandom(b *testing.B) {
keys := make([]string, 100)
for i := range keys {
keys[i] = fmt.Sprintf("%x", sha1.Sum([]byte(fmt.Sprint(i))))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := new(Map).Init()
for _, k := range keys {
m.Add(k, 1)
}
}
}
func BenchmarkMapAddSameSteadyState(b *testing.B) {
m := new(Map).Init()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Add("red", 1)
}
})
}
func BenchmarkMapAddDifferentSteadyState(b *testing.B) {
procKeys := make([][]string, runtime.GOMAXPROCS(0))
for i := range procKeys {
keys := make([]string, 4)
for j := range keys {
keys[j] = fmt.Sprint(i, j)
}
procKeys[i] = keys
}
m := new(Map).Init()
b.ResetTimer()
var n int32
b.RunParallel(func(pb *testing.PB) {
i := int(atomic.AddInt32(&n, 1)-1) % len(procKeys)
keys := procKeys[i]
for pb.Next() {
for _, k := range keys {
m.Add(k, 1)
}
}
})
}
func TestFunc(t *testing.T) {
RemoveAll()
var x any = []string{"a", "b"}
f := Func(func() any { return x })
if s, exp := f.String(), `["a","b"]`; s != exp {
t.Errorf(`f.String() = %q, want %q`, s, exp)
}
if v := f.Value(); !reflect.DeepEqual(v, x) {
t.Errorf(`f.Value() = %q, want %q`, v, x)
}
x = 17
if s, exp := f.String(), `17`; s != exp {
t.Errorf(`f.String() = %q, want %q`, s, exp)
}
}
func TestHandler(t *testing.T) {
RemoveAll()
m := NewMap("map1")
m.Add("a", 1)
m.Add("z", 2)
m2 := NewMap("map2")
for i := 0; i < 9; i++ {
m2.Add(strconv.Itoa(i), int64(i))
}
rr := httptest.NewRecorder()
rr.Body = new(bytes.Buffer)
expvarHandler(rr, nil)
want := `{
"map1": {"a": 1, "z": 2},
"map2": {"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8}
}
`
if got := rr.Body.String(); got != want {
t.Errorf("HTTP handler wrote:\n%s\nWant:\n%s", got, want)
}
}
func BenchmarkMapString(b *testing.B) {
var m, m1, m2 Map
m.Set("map1", &m1)
m1.Add("a", 1)
m1.Add("z", 2)
m.Set("map2", &m2)
for i := 0; i < 9; i++ {
m2.Add(strconv.Itoa(i), int64(i))
}
var s1, s2 String
m.Set("str1", &s1)
s1.Set("hello, world!")
m.Set("str2", &s2)
s2.Set("fizz buzz")
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = m.String()
}
}
func BenchmarkRealworldExpvarUsage(b *testing.B) {
var (
bytesSent Int
bytesRead Int
)
// The benchmark creates GOMAXPROCS client/server pairs.
// Each pair creates 4 goroutines: client reader/writer and server reader/writer.
// The benchmark stresses concurrent reading and writing to the same connection.
// Such pattern is used in net/http and net/rpc.
b.StopTimer()
P := runtime.GOMAXPROCS(0)
N := b.N / P
W := 1000
// Setup P client/server connections.
clients := make([]net.Conn, P)
servers := make([]net.Conn, P)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
b.Fatalf("Listen failed: %v", err)
}
defer ln.Close()
done := make(chan bool, 1)
go func() {
for p := 0; p < P; p++ {
s, err := ln.Accept()
if err != nil {
b.Errorf("Accept failed: %v", err)
done <- false
return
}
servers[p] = s
}
done <- true
}()
for p := 0; p < P; p++ {
c, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
<-done
b.Fatalf("Dial failed: %v", err)
}
clients[p] = c
}
if !<-done {
b.FailNow()
}
b.StartTimer()
var wg sync.WaitGroup
wg.Add(4 * P)
for p := 0; p < P; p++ {
// Client writer.
go func(c net.Conn) {
defer wg.Done()
var buf [1]byte
for i := 0; i < N; i++ {
v := byte(i)
for w := 0; w < W; w++ {
v *= v
}
buf[0] = v
n, err := c.Write(buf[:])
if err != nil {
b.Errorf("Write failed: %v", err)
return
}
bytesSent.Add(int64(n))
}
}(clients[p])
// Pipe between server reader and server writer.
pipe := make(chan byte, 128)
// Server reader.
go func(s net.Conn) {
defer wg.Done()
var buf [1]byte
for i := 0; i < N; i++ {
n, err := s.Read(buf[:])
if err != nil {
b.Errorf("Read failed: %v", err)
return
}
bytesRead.Add(int64(n))
pipe <- buf[0]
}
}(servers[p])
// Server writer.
go func(s net.Conn) {
defer wg.Done()
var buf [1]byte
for i := 0; i < N; i++ {
v := <-pipe
for w := 0; w < W; w++ {
v *= v
}
buf[0] = v
n, err := s.Write(buf[:])
if err != nil {
b.Errorf("Write failed: %v", err)
return
}
bytesSent.Add(int64(n))
}
s.Close()
}(servers[p])
// Client reader.
go func(c net.Conn) {
defer wg.Done()
var buf [1]byte
for i := 0; i < N; i++ {
n, err := c.Read(buf[:])
if err != nil {
b.Errorf("Read failed: %v", err)
return
}
bytesRead.Add(int64(n))
}
c.Close()
}(clients[p])
}
wg.Wait()
}
func TestAppendJSONQuote(t *testing.T) {
var b []byte
for i := 0; i < 128; i++ {
b = append(b, byte(i))
}
b = append(b, "\u2028\u2029"...)
got := string(appendJSONQuote(nil, string(b[:])))
want := `"` +
`\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\t\n\u000b\u000c\r\u000e\u000f` +
`\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
` !\"#$%\u0026'()*+,-./0123456789:;\u003c=\u003e?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_` +
"`" + `abcdefghijklmnopqrstuvwxyz{|}~` + "\x7f" + `\u2028\u2029"`
if got != want {
t.Errorf("appendJSONQuote mismatch:\ngot %v\nwant %v", got, want)
}
}