| // Copyright 2017 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 strings_test |
| |
| import ( |
| "bytes" |
| "internal/asan" |
| . "strings" |
| "testing" |
| "unicode/utf8" |
| ) |
| |
| func check(t *testing.T, b *Builder, want string) { |
| t.Helper() |
| got := b.String() |
| if got != want { |
| t.Errorf("String: got %#q; want %#q", got, want) |
| return |
| } |
| if n := b.Len(); n != len(got) { |
| t.Errorf("Len: got %d; but len(String()) is %d", n, len(got)) |
| } |
| if n := b.Cap(); n < len(got) { |
| t.Errorf("Cap: got %d; but len(String()) is %d", n, len(got)) |
| } |
| } |
| |
| func TestBuilder(t *testing.T) { |
| var b Builder |
| check(t, &b, "") |
| n, err := b.WriteString("hello") |
| if err != nil || n != 5 { |
| t.Errorf("WriteString: got %d,%s; want 5,nil", n, err) |
| } |
| check(t, &b, "hello") |
| if err = b.WriteByte(' '); err != nil { |
| t.Errorf("WriteByte: %s", err) |
| } |
| check(t, &b, "hello ") |
| n, err = b.WriteString("world") |
| if err != nil || n != 5 { |
| t.Errorf("WriteString: got %d,%s; want 5,nil", n, err) |
| } |
| check(t, &b, "hello world") |
| } |
| |
| func TestBuilderString(t *testing.T) { |
| var b Builder |
| b.WriteString("alpha") |
| check(t, &b, "alpha") |
| s1 := b.String() |
| b.WriteString("beta") |
| check(t, &b, "alphabeta") |
| s2 := b.String() |
| b.WriteString("gamma") |
| check(t, &b, "alphabetagamma") |
| s3 := b.String() |
| |
| // Check that subsequent operations didn't change the returned strings. |
| if want := "alpha"; s1 != want { |
| t.Errorf("first String result is now %q; want %q", s1, want) |
| } |
| if want := "alphabeta"; s2 != want { |
| t.Errorf("second String result is now %q; want %q", s2, want) |
| } |
| if want := "alphabetagamma"; s3 != want { |
| t.Errorf("third String result is now %q; want %q", s3, want) |
| } |
| } |
| |
| func TestBuilderReset(t *testing.T) { |
| var b Builder |
| check(t, &b, "") |
| b.WriteString("aaa") |
| s := b.String() |
| check(t, &b, "aaa") |
| b.Reset() |
| check(t, &b, "") |
| |
| // Ensure that writing after Reset doesn't alter |
| // previously returned strings. |
| b.WriteString("bbb") |
| check(t, &b, "bbb") |
| if want := "aaa"; s != want { |
| t.Errorf("previous String result changed after Reset: got %q; want %q", s, want) |
| } |
| } |
| |
| func TestBuilderGrow(t *testing.T) { |
| for _, growLen := range []int{0, 100, 1000, 10000, 100000} { |
| if asan.Enabled { |
| t.Logf("skipping allocs check for growLen %d: extra allocs with -asan; see #70079", growLen) |
| continue |
| } |
| p := bytes.Repeat([]byte{'a'}, growLen) |
| allocs := testing.AllocsPerRun(100, func() { |
| var b Builder |
| b.Grow(growLen) // should be only alloc, when growLen > 0 |
| if b.Cap() < growLen { |
| t.Fatalf("growLen=%d: Cap() is lower than growLen", growLen) |
| } |
| b.Write(p) |
| if b.String() != string(p) { |
| t.Fatalf("growLen=%d: bad data written after Grow", growLen) |
| } |
| }) |
| wantAllocs := 1 |
| if growLen == 0 { |
| wantAllocs = 0 |
| } |
| if g, w := int(allocs), wantAllocs; g != w { |
| t.Errorf("growLen=%d: got %d allocs during Write; want %v", growLen, g, w) |
| } |
| } |
| // when growLen < 0, should panic |
| var a Builder |
| n := -1 |
| defer func() { |
| if r := recover(); r == nil { |
| t.Errorf("a.Grow(%d) should panic()", n) |
| } |
| }() |
| a.Grow(n) |
| } |
| |
| func TestBuilderWrite2(t *testing.T) { |
| const s0 = "hello 世界" |
| for _, tt := range []struct { |
| name string |
| fn func(b *Builder) (int, error) |
| n int |
| want string |
| }{ |
| { |
| "Write", |
| func(b *Builder) (int, error) { return b.Write([]byte(s0)) }, |
| len(s0), |
| s0, |
| }, |
| { |
| "WriteRune", |
| func(b *Builder) (int, error) { return b.WriteRune('a') }, |
| 1, |
| "a", |
| }, |
| { |
| "WriteRuneWide", |
| func(b *Builder) (int, error) { return b.WriteRune('世') }, |
| 3, |
| "世", |
| }, |
| { |
| "WriteString", |
| func(b *Builder) (int, error) { return b.WriteString(s0) }, |
| len(s0), |
| s0, |
| }, |
| } { |
| t.Run(tt.name, func(t *testing.T) { |
| var b Builder |
| n, err := tt.fn(&b) |
| if err != nil { |
| t.Fatalf("first call: got %s", err) |
| } |
| if n != tt.n { |
| t.Errorf("first call: got n=%d; want %d", n, tt.n) |
| } |
| check(t, &b, tt.want) |
| |
| n, err = tt.fn(&b) |
| if err != nil { |
| t.Fatalf("second call: got %s", err) |
| } |
| if n != tt.n { |
| t.Errorf("second call: got n=%d; want %d", n, tt.n) |
| } |
| check(t, &b, tt.want+tt.want) |
| }) |
| } |
| } |
| |
| func TestBuilderWriteByte(t *testing.T) { |
| var b Builder |
| if err := b.WriteByte('a'); err != nil { |
| t.Error(err) |
| } |
| if err := b.WriteByte(0); err != nil { |
| t.Error(err) |
| } |
| check(t, &b, "a\x00") |
| } |
| |
| func TestBuilderAllocs(t *testing.T) { |
| if asan.Enabled { |
| t.Skip("test allocates more with -asan; see #70079") |
| } |
| // Issue 23382; verify that copyCheck doesn't force the |
| // Builder to escape and be heap allocated. |
| n := testing.AllocsPerRun(10000, func() { |
| var b Builder |
| b.Grow(5) |
| b.WriteString("abcde") |
| _ = b.String() |
| }) |
| if n != 1 { |
| t.Errorf("Builder allocs = %v; want 1", n) |
| } |
| } |
| |
| func TestBuilderCopyPanic(t *testing.T) { |
| tests := []struct { |
| name string |
| fn func() |
| wantPanic bool |
| }{ |
| { |
| name: "String", |
| wantPanic: false, |
| fn: func() { |
| var a Builder |
| a.WriteByte('x') |
| b := a |
| _ = b.String() // appease vet |
| }, |
| }, |
| { |
| name: "Len", |
| wantPanic: false, |
| fn: func() { |
| var a Builder |
| a.WriteByte('x') |
| b := a |
| b.Len() |
| }, |
| }, |
| { |
| name: "Cap", |
| wantPanic: false, |
| fn: func() { |
| var a Builder |
| a.WriteByte('x') |
| b := a |
| b.Cap() |
| }, |
| }, |
| { |
| name: "Reset", |
| wantPanic: false, |
| fn: func() { |
| var a Builder |
| a.WriteByte('x') |
| b := a |
| b.Reset() |
| b.WriteByte('y') |
| }, |
| }, |
| { |
| name: "Write", |
| wantPanic: true, |
| fn: func() { |
| var a Builder |
| a.Write([]byte("x")) |
| b := a |
| b.Write([]byte("y")) |
| }, |
| }, |
| { |
| name: "WriteByte", |
| wantPanic: true, |
| fn: func() { |
| var a Builder |
| a.WriteByte('x') |
| b := a |
| b.WriteByte('y') |
| }, |
| }, |
| { |
| name: "WriteString", |
| wantPanic: true, |
| fn: func() { |
| var a Builder |
| a.WriteString("x") |
| b := a |
| b.WriteString("y") |
| }, |
| }, |
| { |
| name: "WriteRune", |
| wantPanic: true, |
| fn: func() { |
| var a Builder |
| a.WriteRune('x') |
| b := a |
| b.WriteRune('y') |
| }, |
| }, |
| { |
| name: "Grow", |
| wantPanic: true, |
| fn: func() { |
| var a Builder |
| a.Grow(1) |
| b := a |
| b.Grow(2) |
| }, |
| }, |
| } |
| for _, tt := range tests { |
| didPanic := make(chan bool) |
| go func() { |
| defer func() { didPanic <- recover() != nil }() |
| tt.fn() |
| }() |
| if got := <-didPanic; got != tt.wantPanic { |
| t.Errorf("%s: panicked = %v; want %v", tt.name, got, tt.wantPanic) |
| } |
| } |
| } |
| |
| func TestBuilderWriteInvalidRune(t *testing.T) { |
| // Invalid runes, including negative ones, should be written as |
| // utf8.RuneError. |
| for _, r := range []rune{-1, utf8.MaxRune + 1} { |
| var b Builder |
| b.WriteRune(r) |
| check(t, &b, "\uFFFD") |
| } |
| } |
| |
| var someBytes = []byte("some bytes sdljlk jsklj3lkjlk djlkjw") |
| |
| var sinkS string |
| |
| func benchmarkBuilder(b *testing.B, f func(b *testing.B, numWrite int, grow bool)) { |
| b.Run("1Write_NoGrow", func(b *testing.B) { |
| b.ReportAllocs() |
| f(b, 1, false) |
| }) |
| b.Run("3Write_NoGrow", func(b *testing.B) { |
| b.ReportAllocs() |
| f(b, 3, false) |
| }) |
| b.Run("3Write_Grow", func(b *testing.B) { |
| b.ReportAllocs() |
| f(b, 3, true) |
| }) |
| } |
| |
| func BenchmarkBuildString_Builder(b *testing.B) { |
| benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) { |
| for i := 0; i < b.N; i++ { |
| var buf Builder |
| if grow { |
| buf.Grow(len(someBytes) * numWrite) |
| } |
| for i := 0; i < numWrite; i++ { |
| buf.Write(someBytes) |
| } |
| sinkS = buf.String() |
| } |
| }) |
| } |
| |
| func BenchmarkBuildString_WriteString(b *testing.B) { |
| someString := string(someBytes) |
| benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) { |
| for i := 0; i < b.N; i++ { |
| var buf Builder |
| if grow { |
| buf.Grow(len(someString) * numWrite) |
| } |
| for i := 0; i < numWrite; i++ { |
| buf.WriteString(someString) |
| } |
| sinkS = buf.String() |
| } |
| }) |
| } |
| |
| func BenchmarkBuildString_ByteBuffer(b *testing.B) { |
| benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) { |
| for i := 0; i < b.N; i++ { |
| var buf bytes.Buffer |
| if grow { |
| buf.Grow(len(someBytes) * numWrite) |
| } |
| for i := 0; i < numWrite; i++ { |
| buf.Write(someBytes) |
| } |
| sinkS = buf.String() |
| } |
| }) |
| } |
| |
| func TestBuilderGrowSizeclasses(t *testing.T) { |
| if asan.Enabled { |
| t.Skip("test allocates more with -asan; see #70079") |
| } |
| s := Repeat("a", 19) |
| allocs := testing.AllocsPerRun(100, func() { |
| var b Builder |
| b.Grow(18) |
| b.WriteString(s) |
| _ = b.String() |
| }) |
| if allocs > 1 { |
| t.Fatalf("unexpected amount of allocations: %v, want: 1", allocs) |
| } |
| } |