github.com/go-darwin/sys@v0.0.0-20220510002607-68fd01f054ca/string_test.go (about) 1 // Copyright 2012 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package sys_test 6 7 import ( 8 "bytes" 9 "fmt" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "runtime" 14 _ "runtime" // for go:linkname 15 "strconv" 16 "strings" 17 "sync" 18 "syscall" 19 "testing" 20 "time" 21 "unicode/utf8" 22 _ "unsafe" // for go:linkname 23 24 "github.com/go-darwin/sys" 25 "github.com/go-darwin/sys/testenv" 26 ) 27 28 // Strings and slices that don't escape and fit into tmpBuf are stack allocated, 29 // which defeats using AllocsPerRun to test other optimizations. 30 const sizeNoStack = 100 31 32 func BenchmarkCompareStringEqual(b *testing.B) { 33 bytes := []byte("Hello Gophers!") 34 s1, s2 := string(bytes), string(bytes) 35 for i := 0; i < b.N; i++ { 36 if s1 != s2 { 37 b.Fatal("s1 != s2") 38 } 39 } 40 } 41 42 func BenchmarkCompareStringIdentical(b *testing.B) { 43 s1 := "Hello Gophers!" 44 s2 := s1 45 for i := 0; i < b.N; i++ { 46 if s1 != s2 { 47 b.Fatal("s1 != s2") 48 } 49 } 50 } 51 52 func BenchmarkCompareStringSameLength(b *testing.B) { 53 s1 := "Hello Gophers!" 54 s2 := "Hello, Gophers" 55 for i := 0; i < b.N; i++ { 56 if s1 == s2 { 57 b.Fatal("s1 == s2") 58 } 59 } 60 } 61 62 func BenchmarkCompareStringDifferentLength(b *testing.B) { 63 s1 := "Hello Gophers!" 64 s2 := "Hello, Gophers!" 65 for i := 0; i < b.N; i++ { 66 if s1 == s2 { 67 b.Fatal("s1 == s2") 68 } 69 } 70 } 71 72 func BenchmarkCompareStringBigUnaligned(b *testing.B) { 73 bytes := make([]byte, 0, 1<<20) 74 for len(bytes) < 1<<20 { 75 bytes = append(bytes, "Hello Gophers!"...) 76 } 77 s1, s2 := string(bytes), "hello"+string(bytes) 78 for i := 0; i < b.N; i++ { 79 if s1 != s2[len("hello"):] { 80 b.Fatal("s1 != s2") 81 } 82 } 83 b.SetBytes(int64(len(s1))) 84 } 85 86 func BenchmarkCompareStringBig(b *testing.B) { 87 bytes := make([]byte, 0, 1<<20) 88 for len(bytes) < 1<<20 { 89 bytes = append(bytes, "Hello Gophers!"...) 90 } 91 s1, s2 := string(bytes), string(bytes) 92 for i := 0; i < b.N; i++ { 93 if s1 != s2 { 94 b.Fatal("s1 != s2") 95 } 96 } 97 b.SetBytes(int64(len(s1))) 98 } 99 100 func BenchmarkConcatStringAndBytes(b *testing.B) { 101 s1 := []byte("Gophers!") 102 for i := 0; i < b.N; i++ { 103 _ = "Hello " + string(s1) 104 } 105 } 106 107 var escapeString string 108 109 func BenchmarkSliceByteToString(b *testing.B) { 110 buf := []byte{'!'} 111 for n := 0; n < 8; n++ { 112 b.Run(strconv.Itoa(len(buf)), func(b *testing.B) { 113 for i := 0; i < b.N; i++ { 114 escapeString = string(buf) 115 } 116 }) 117 buf = append(buf, buf...) 118 } 119 } 120 121 var stringdata = []struct{ name, data string }{ 122 {"ASCII", "01234567890"}, 123 {"Japanese", "日本語日本語日本語"}, 124 {"MixedLength", "$Ѐࠀက퀀𐀀\U00040000\U0010FFFF"}, 125 } 126 127 var sinkInt int 128 129 func BenchmarkRuneCount(b *testing.B) { 130 // Each sub-benchmark counts the runes in a string in a different way. 131 b.Run("lenruneslice", func(b *testing.B) { 132 for _, sd := range stringdata { 133 b.Run(sd.name, func(b *testing.B) { 134 for i := 0; i < b.N; i++ { 135 sinkInt += len([]rune(sd.data)) 136 } 137 }) 138 } 139 }) 140 b.Run("rangeloop", func(b *testing.B) { 141 for _, sd := range stringdata { 142 b.Run(sd.name, func(b *testing.B) { 143 for i := 0; i < b.N; i++ { 144 n := 0 145 for range sd.data { 146 n++ 147 } 148 sinkInt += n 149 } 150 }) 151 } 152 }) 153 b.Run("utf8.RuneCountInString", func(b *testing.B) { 154 for _, sd := range stringdata { 155 b.Run(sd.name, func(b *testing.B) { 156 for i := 0; i < b.N; i++ { 157 sinkInt += utf8.RuneCountInString(sd.data) 158 } 159 }) 160 } 161 }) 162 } 163 164 func BenchmarkRuneIterate(b *testing.B) { 165 b.Run("range", func(b *testing.B) { 166 for _, sd := range stringdata { 167 b.Run(sd.name, func(b *testing.B) { 168 for i := 0; i < b.N; i++ { 169 for range sd.data { 170 } 171 } 172 }) 173 } 174 }) 175 b.Run("range1", func(b *testing.B) { 176 for _, sd := range stringdata { 177 b.Run(sd.name, func(b *testing.B) { 178 for i := 0; i < b.N; i++ { 179 for range sd.data { 180 } 181 } 182 }) 183 } 184 }) 185 b.Run("range2", func(b *testing.B) { 186 for _, sd := range stringdata { 187 b.Run(sd.name, func(b *testing.B) { 188 for i := 0; i < b.N; i++ { 189 for range sd.data { 190 } 191 } 192 }) 193 } 194 }) 195 } 196 197 func BenchmarkArrayEqual(b *testing.B) { 198 a1 := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 199 a2 := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 200 b.ResetTimer() 201 for i := 0; i < b.N; i++ { 202 if a1 != a2 { 203 b.Fatal("not equal") 204 } 205 } 206 } 207 208 // entry point for testing 209 func GostringW(w []uint16) (s string) { 210 s = sys.GoStringW(&w[0]) 211 return 212 } 213 214 func TestStringW(t *testing.T) { 215 strs := []string{ 216 "hello", 217 "a\u5566\u7788b", 218 } 219 220 for _, s := range strs { 221 var b []uint16 222 for _, c := range s { 223 b = append(b, uint16(c)) 224 if c != rune(uint16(c)) { 225 t.Errorf("bad test: stringW can't handle >16 bit runes") 226 } 227 } 228 b = append(b, 0) 229 r := GostringW(b) 230 if r != s { 231 t.Errorf("gostringW(%v) = %s, want %s", b, r, s) 232 } 233 } 234 } 235 236 var toRemove []string 237 238 func TestMain(m *testing.M) { 239 status := m.Run() 240 for _, file := range toRemove { 241 os.RemoveAll(file) 242 } 243 os.Exit(status) 244 } 245 246 var testprog struct { 247 sync.Mutex 248 dir string 249 target map[string]buildexe 250 } 251 252 type buildexe struct { 253 exe string 254 err error 255 } 256 257 func buildTestProg(t *testing.T, binary string, flags ...string) (string, error) { 258 if *flagQuick { 259 t.Skip("-quick") 260 } 261 262 testprog.Lock() 263 defer testprog.Unlock() 264 if testprog.dir == "" { 265 dir, err := os.MkdirTemp("", "go-build") 266 if err != nil { 267 t.Fatalf("failed to create temp directory: %v", err) 268 } 269 testprog.dir = dir 270 toRemove = append(toRemove, dir) 271 } 272 273 if testprog.target == nil { 274 testprog.target = make(map[string]buildexe) 275 } 276 name := binary 277 if len(flags) > 0 { 278 name += "_" + strings.Join(flags, "_") 279 } 280 target, ok := testprog.target[name] 281 if ok { 282 return target.exe, target.err 283 } 284 285 exe := filepath.Join(testprog.dir, name+".exe") 286 cmd := exec.Command(testenv.GoToolPath(t), append([]string{"build", "-o", exe}, flags...)...) 287 cmd.Dir = "testdata/" + binary 288 out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() 289 if err != nil { 290 target.err = fmt.Errorf("building %s %v: %v\n%s", binary, flags, err, out) 291 testprog.target[name] = target 292 return "", target.err 293 } 294 target.exe = exe 295 testprog.target[name] = target 296 return exe, nil 297 } 298 299 func runTestProg(t *testing.T, binary, name string, env ...string) string { 300 if *flagQuick { 301 t.Skip("-quick") 302 } 303 304 testenv.MustHaveGoBuild(t) 305 306 exe, err := buildTestProg(t, binary) 307 if err != nil { 308 t.Fatal(err) 309 } 310 311 return runBuiltTestProg(t, exe, name, env...) 312 } 313 314 // sigquit is the signal to send to kill a hanging testdata program. 315 // Send SIGQUIT to get a stack trace. 316 var sigquit = syscall.SIGQUIT 317 318 func runBuiltTestProg(t *testing.T, exe, name string, env ...string) string { 319 if *flagQuick { 320 t.Skip("-quick") 321 } 322 323 testenv.MustHaveGoBuild(t) 324 325 cmd := testenv.CleanCmdEnv(exec.Command(exe, name)) 326 cmd.Env = append(cmd.Env, env...) 327 if testing.Short() { 328 cmd.Env = append(cmd.Env, "RUNTIME_TEST_SHORT=1") 329 } 330 var b bytes.Buffer 331 cmd.Stdout = &b 332 cmd.Stderr = &b 333 if err := cmd.Start(); err != nil { 334 t.Fatalf("starting %s %s: %v", exe, name, err) 335 } 336 337 // If the process doesn't complete within 1 minute, 338 // assume it is hanging and kill it to get a stack trace. 339 p := cmd.Process 340 done := make(chan bool) 341 go func() { 342 scale := 1 343 // This GOARCH/GOOS test is copied from cmd/dist/test.go. 344 // TODO(iant): Have cmd/dist update the environment variable. 345 if runtime.GOARCH == "arm" || runtime.GOOS == "windows" { 346 scale = 2 347 } 348 if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { 349 if sc, err := strconv.Atoi(s); err == nil { 350 scale = sc 351 } 352 } 353 354 select { 355 case <-done: 356 case <-time.After(time.Duration(scale) * time.Minute): 357 p.Signal(sigquit) 358 } 359 }() 360 361 if err := cmd.Wait(); err != nil { 362 t.Logf("%s %s exit status: %v", exe, name, err) 363 } 364 close(done) 365 366 return b.String() 367 } 368 369 func TestLargeStringConcat(t *testing.T) { 370 output := runTestProg(t, "testprog", "stringconcat") 371 want := "panic: " + strings.Repeat("0", 1<<10) + strings.Repeat("1", 1<<10) + 372 strings.Repeat("2", 1<<10) + strings.Repeat("3", 1<<10) 373 if !strings.HasPrefix(output, want) { 374 t.Fatalf("output does not start with %q:\n%s", want, output) 375 } 376 } 377 378 func TestCompareTempString(t *testing.T) { 379 s := strings.Repeat("x", sizeNoStack) 380 b := []byte(s) 381 n := testing.AllocsPerRun(1000, func() { 382 if string(b) != s { 383 t.Fatalf("strings are not equal: '%v' and '%v'", string(b), s) 384 } 385 if string(b) == s { 386 } else { 387 t.Fatalf("strings are not equal: '%v' and '%v'", string(b), s) 388 } 389 }) 390 if n != 0 { 391 t.Fatalf("want 0 allocs, got %v", n) 392 } 393 } 394 395 func TestStringIndexHaystack(t *testing.T) { 396 // See issue 25864. 397 haystack := []byte("hello") 398 needle := "ll" 399 n := testing.AllocsPerRun(1000, func() { 400 if strings.Index(string(haystack), needle) != 2 { 401 t.Fatalf("needle not found") 402 } 403 }) 404 if n != 0 { 405 t.Fatalf("want 0 allocs, got %v", n) 406 } 407 } 408 409 func TestStringIndexNeedle(t *testing.T) { 410 // See issue 25864. 411 haystack := "hello" 412 needle := []byte("ll") 413 n := testing.AllocsPerRun(1000, func() { 414 if strings.Index(haystack, string(needle)) != 2 { 415 t.Fatalf("needle not found") 416 } 417 }) 418 if n != 0 { 419 t.Fatalf("want 0 allocs, got %v", n) 420 } 421 } 422 423 func TestStringOnStack(t *testing.T) { 424 s := "" 425 for i := 0; i < 3; i++ { 426 s = "a" + s + "b" + s + "c" 427 } 428 429 if want := "aaabcbabccbaabcbabccc"; s != want { 430 t.Fatalf("want: '%v', got '%v'", want, s) 431 } 432 } 433 434 func TestIntString(t *testing.T) { 435 // Non-escaping result of intstring. 436 s := "" 437 for i := rune(0); i < 4; i++ { 438 s += string(i+'0') + string(i+'0'+1) 439 } 440 if want := "01122334"; s != want { 441 t.Fatalf("want '%v', got '%v'", want, s) 442 } 443 444 // Escaping result of intstring. 445 var a [4]string 446 for i := rune(0); i < 4; i++ { 447 a[i] = string(i + '0') 448 } 449 s = a[0] + a[1] + a[2] + a[3] 450 if want := "0123"; s != want { 451 t.Fatalf("want '%v', got '%v'", want, s) 452 } 453 } 454 455 func TestIntStringAllocs(t *testing.T) { 456 unknown := '0' 457 n := testing.AllocsPerRun(1000, func() { 458 s1 := string(unknown) 459 s2 := string(unknown + 1) 460 if s1 == s2 { 461 t.Fatalf("bad") 462 } 463 }) 464 if n != 0 { 465 t.Fatalf("want 0 allocs, got %v", n) 466 } 467 } 468 469 func TestRangeStringCast(t *testing.T) { 470 s := strings.Repeat("x", sizeNoStack) 471 n := testing.AllocsPerRun(1000, func() { 472 for i, c := range []byte(s) { 473 if c != s[i] { 474 t.Fatalf("want '%c' at pos %v, got '%c'", s[i], i, c) 475 } 476 } 477 }) 478 if n != 0 { 479 t.Fatalf("want 0 allocs, got %v", n) 480 } 481 } 482 483 func isZeroed(b []byte) bool { 484 for _, x := range b { 485 if x != 0 { 486 return false 487 } 488 } 489 return true 490 } 491 492 func isZeroedR(r []rune) bool { 493 for _, x := range r { 494 if x != 0 { 495 return false 496 } 497 } 498 return true 499 } 500 501 func TestString2Slice(t *testing.T) { 502 // Make sure we don't return slices that expose 503 // an unzeroed section of stack-allocated temp buf 504 // between len and cap. See issue 14232. 505 s := "foož" 506 b := ([]byte)(s) 507 if !isZeroed(b[len(b):cap(b)]) { 508 t.Errorf("extra bytes not zeroed") 509 } 510 r := ([]rune)(s) 511 if !isZeroedR(r[len(r):cap(r)]) { 512 t.Errorf("extra runes not zeroed") 513 } 514 }