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  }