// Copyright 2012 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. //go:build cgo package runtime_test import ( "fmt" "internal/goos" "internal/platform" "internal/testenv" "os" "os/exec" "runtime" "strconv" "strings" "testing" "time" ) func TestCgoCrashHandler(t *testing.T) { t.Parallel() testCrashHandler(t, true) } func TestCgoSignalDeadlock(t *testing.T) { // Don't call t.Parallel, since too much work going on at the // same time can cause the testprogcgo code to overrun its // timeouts (issue #18598). if testing.Short() && runtime.GOOS == "windows" { t.Skip("Skipping in short mode") // takes up to 64 seconds } got := runTestProg(t, "testprogcgo", "CgoSignalDeadlock") want := "OK\n" if got != want { t.Fatalf("expected %q, but got:\n%s", want, got) } } func TestCgoTraceback(t *testing.T) { t.Parallel() got := runTestProg(t, "testprogcgo", "CgoTraceback") want := "OK\n" if got != want { t.Fatalf("expected %q, but got:\n%s", want, got) } } func TestCgoCallbackGC(t *testing.T) { t.Parallel() switch runtime.GOOS { case "plan9", "windows": t.Skipf("no pthreads on %s", runtime.GOOS) } if testing.Short() { switch { case runtime.GOOS == "dragonfly": t.Skip("see golang.org/issue/11990") case runtime.GOOS == "linux" && runtime.GOARCH == "arm": t.Skip("too slow for arm builders") case runtime.GOOS == "linux" && (runtime.GOARCH == "mips64" || runtime.GOARCH == "mips64le"): t.Skip("too slow for mips64x builders") } } got := runTestProg(t, "testprogcgo", "CgoCallbackGC") want := "OK\n" if got != want { t.Fatalf("expected %q, but got:\n%s", want, got) } } func TestCgoExternalThreadPanic(t *testing.T) { t.Parallel() if runtime.GOOS == "plan9" { t.Skipf("no pthreads on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "CgoExternalThreadPanic") want := "panic: BOOM" if !strings.Contains(got, want) { t.Fatalf("want failure containing %q. output:\n%s\n", want, got) } } func TestCgoExternalThreadSIGPROF(t *testing.T) { t.Parallel() // issue 9456. switch runtime.GOOS { case "plan9", "windows": t.Skipf("no pthreads on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "CgoExternalThreadSIGPROF", "GO_START_SIGPROF_THREAD=1") if want := "OK\n"; got != want { t.Fatalf("expected %q, but got:\n%s", want, got) } } func TestCgoExternalThreadSignal(t *testing.T) { t.Parallel() // issue 10139 switch runtime.GOOS { case "plan9", "windows": t.Skipf("no pthreads on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "CgoExternalThreadSignal") if want := "OK\n"; got != want { if runtime.GOOS == "ios" && strings.Contains(got, "C signal did not crash as expected") { testenv.SkipFlaky(t, 59913) } t.Fatalf("expected %q, but got:\n%s", want, got) } } func TestCgoDLLImports(t *testing.T) { // test issue 9356 if runtime.GOOS != "windows" { t.Skip("skipping windows specific test") } got := runTestProg(t, "testprogcgo", "CgoDLLImportsMain") want := "OK\n" if got != want { t.Fatalf("expected %q, but got %v", want, got) } } func TestCgoExecSignalMask(t *testing.T) { t.Parallel() // Test issue 13164. switch runtime.GOOS { case "windows", "plan9": t.Skipf("skipping signal mask test on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "CgoExecSignalMask", "GOTRACEBACK=system") want := "OK\n" if got != want { t.Errorf("expected %q, got %v", want, got) } } func TestEnsureDropM(t *testing.T) { t.Parallel() // Test for issue 13881. switch runtime.GOOS { case "windows", "plan9": t.Skipf("skipping dropm test on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "EnsureDropM") want := "OK\n" if got != want { t.Errorf("expected %q, got %v", want, got) } } // Test for issue 14387. // Test that the program that doesn't need any cgo pointer checking // takes about the same amount of time with it as without it. func TestCgoCheckBytes(t *testing.T) { t.Parallel() // Make sure we don't count the build time as part of the run time. testenv.MustHaveGoBuild(t) exe, err := buildTestProg(t, "testprogcgo") if err != nil { t.Fatal(err) } // Try it 10 times to avoid flakiness. const tries = 10 var tot1, tot2 time.Duration for i := 0; i < tries; i++ { cmd := testenv.CleanCmdEnv(exec.Command(exe, "CgoCheckBytes")) cmd.Env = append(cmd.Env, "GODEBUG=cgocheck=0", fmt.Sprintf("GO_CGOCHECKBYTES_TRY=%d", i)) start := time.Now() cmd.Run() d1 := time.Since(start) cmd = testenv.CleanCmdEnv(exec.Command(exe, "CgoCheckBytes")) cmd.Env = append(cmd.Env, fmt.Sprintf("GO_CGOCHECKBYTES_TRY=%d", i)) start = time.Now() cmd.Run() d2 := time.Since(start) if d1*20 > d2 { // The slow version (d2) was less than 20 times // slower than the fast version (d1), so OK. return } tot1 += d1 tot2 += d2 } t.Errorf("cgo check too slow: got %v, expected at most %v", tot2/tries, (tot1/tries)*20) } func TestCgoPanicDeadlock(t *testing.T) { t.Parallel() // test issue 14432 got := runTestProg(t, "testprogcgo", "CgoPanicDeadlock") want := "panic: cgo error\n\n" if !strings.HasPrefix(got, want) { t.Fatalf("output does not start with %q:\n%s", want, got) } } func TestCgoCCodeSIGPROF(t *testing.T) { t.Parallel() got := runTestProg(t, "testprogcgo", "CgoCCodeSIGPROF") want := "OK\n" if got != want { t.Errorf("expected %q got %v", want, got) } } func TestCgoPprofCallback(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode") // takes a full second } switch runtime.GOOS { case "windows", "plan9": t.Skipf("skipping cgo pprof callback test on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "CgoPprofCallback") want := "OK\n" if got != want { t.Errorf("expected %q got %v", want, got) } } func TestCgoCrashTraceback(t *testing.T) { t.Parallel() switch platform := runtime.GOOS + "/" + runtime.GOARCH; platform { case "darwin/amd64": case "linux/amd64": case "linux/arm64": case "linux/ppc64le": default: t.Skipf("not yet supported on %s", platform) } got := runTestProg(t, "testprogcgo", "CrashTraceback") for i := 1; i <= 3; i++ { if !strings.Contains(got, fmt.Sprintf("cgo symbolizer:%d", i)) { t.Errorf("missing cgo symbolizer:%d", i) } } } func TestCgoCrashTracebackGo(t *testing.T) { t.Parallel() switch platform := runtime.GOOS + "/" + runtime.GOARCH; platform { case "darwin/amd64": case "linux/amd64": case "linux/arm64": case "linux/ppc64le": default: t.Skipf("not yet supported on %s", platform) } got := runTestProg(t, "testprogcgo", "CrashTracebackGo") for i := 1; i <= 3; i++ { want := fmt.Sprintf("main.h%d", i) if !strings.Contains(got, want) { t.Errorf("missing %s", want) } } } func TestCgoTracebackContext(t *testing.T) { t.Parallel() got := runTestProg(t, "testprogcgo", "TracebackContext") want := "OK\n" if got != want { t.Errorf("expected %q got %v", want, got) } } func TestCgoTracebackContextPreemption(t *testing.T) { t.Parallel() got := runTestProg(t, "testprogcgo", "TracebackContextPreemption") want := "OK\n" if got != want { t.Errorf("expected %q got %v", want, got) } } func testCgoPprof(t *testing.T, buildArg, runArg, top, bottom string) { t.Parallel() if runtime.GOOS != "linux" || (runtime.GOARCH != "amd64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "arm64") { t.Skipf("not yet supported on %s/%s", runtime.GOOS, runtime.GOARCH) } testenv.MustHaveGoRun(t) exe, err := buildTestProg(t, "testprogcgo", buildArg) if err != nil { t.Fatal(err) } cmd := testenv.CleanCmdEnv(exec.Command(exe, runArg)) got, err := cmd.CombinedOutput() if err != nil { if testenv.Builder() == "linux-amd64-alpine" { // See Issue 18243 and Issue 19938. t.Skipf("Skipping failing test on Alpine (golang.org/issue/18243). Ignoring error: %v", err) } t.Fatalf("%s\n\n%v", got, err) } fn := strings.TrimSpace(string(got)) defer os.Remove(fn) for try := 0; try < 2; try++ { cmd := testenv.CleanCmdEnv(exec.Command(testenv.GoToolPath(t), "tool", "pprof", "-tagignore=ignore", "-traces")) // Check that pprof works both with and without explicit executable on command line. if try == 0 { cmd.Args = append(cmd.Args, exe, fn) } else { cmd.Args = append(cmd.Args, fn) } found := false for i, e := range cmd.Env { if strings.HasPrefix(e, "PPROF_TMPDIR=") { cmd.Env[i] = "PPROF_TMPDIR=" + os.TempDir() found = true break } } if !found { cmd.Env = append(cmd.Env, "PPROF_TMPDIR="+os.TempDir()) } out, err := cmd.CombinedOutput() t.Logf("%s:\n%s", cmd.Args, out) if err != nil { t.Error(err) continue } trace := findTrace(string(out), top) if len(trace) == 0 { t.Errorf("%s traceback missing.", top) continue } if trace[len(trace)-1] != bottom { t.Errorf("invalid traceback origin: got=%v; want=[%s ... %s]", trace, top, bottom) } } } func TestCgoPprof(t *testing.T) { testCgoPprof(t, "", "CgoPprof", "cpuHog", "runtime.main") } func TestCgoPprofPIE(t *testing.T) { testCgoPprof(t, "-buildmode=pie", "CgoPprof", "cpuHog", "runtime.main") } func TestCgoPprofThread(t *testing.T) { testCgoPprof(t, "", "CgoPprofThread", "cpuHogThread", "cpuHogThread2") } func TestCgoPprofThreadNoTraceback(t *testing.T) { testCgoPprof(t, "", "CgoPprofThreadNoTraceback", "cpuHogThread", "runtime._ExternalCode") } func TestRaceProf(t *testing.T) { if !platform.RaceDetectorSupported(runtime.GOOS, runtime.GOARCH) { t.Skipf("skipping on %s/%s because race detector not supported", runtime.GOOS, runtime.GOARCH) } if runtime.GOOS == "windows" { t.Skipf("skipping: test requires pthread support") // TODO: Can this test be rewritten to use the C11 thread API instead? } testenv.MustHaveGoRun(t) // This test requires building various packages with -race, so // it's somewhat slow. if testing.Short() { t.Skip("skipping test in -short mode") } exe, err := buildTestProg(t, "testprogcgo", "-race") if err != nil { t.Fatal(err) } got, err := testenv.CleanCmdEnv(exec.Command(exe, "CgoRaceprof")).CombinedOutput() if err != nil { t.Fatal(err) } want := "OK\n" if string(got) != want { t.Errorf("expected %q got %s", want, got) } } func TestRaceSignal(t *testing.T) { if !platform.RaceDetectorSupported(runtime.GOOS, runtime.GOARCH) { t.Skipf("skipping on %s/%s because race detector not supported", runtime.GOOS, runtime.GOARCH) } if runtime.GOOS == "windows" { t.Skipf("skipping: test requires pthread support") // TODO: Can this test be rewritten to use the C11 thread API instead? } if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { testenv.SkipFlaky(t, 60316) } t.Parallel() testenv.MustHaveGoRun(t) // This test requires building various packages with -race, so // it's somewhat slow. if testing.Short() { t.Skip("skipping test in -short mode") } exe, err := buildTestProg(t, "testprogcgo", "-race") if err != nil { t.Fatal(err) } got, err := testenv.CleanCmdEnv(testenv.Command(t, exe, "CgoRaceSignal")).CombinedOutput() if err != nil { t.Logf("%s\n", got) t.Fatal(err) } want := "OK\n" if string(got) != want { t.Errorf("expected %q got %s", want, got) } } func TestCgoNumGoroutine(t *testing.T) { switch runtime.GOOS { case "windows", "plan9": t.Skipf("skipping numgoroutine test on %s", runtime.GOOS) } t.Parallel() got := runTestProg(t, "testprogcgo", "NumGoroutine") want := "OK\n" if got != want { t.Errorf("expected %q got %v", want, got) } } func TestCatchPanic(t *testing.T) { t.Parallel() switch runtime.GOOS { case "plan9", "windows": t.Skipf("no signals on %s", runtime.GOOS) case "darwin": if runtime.GOARCH == "amd64" { t.Skipf("crash() on darwin/amd64 doesn't raise SIGABRT") } } testenv.MustHaveGoRun(t) exe, err := buildTestProg(t, "testprogcgo") if err != nil { t.Fatal(err) } for _, early := range []bool{true, false} { cmd := testenv.CleanCmdEnv(exec.Command(exe, "CgoCatchPanic")) // Make sure a panic results in a crash. cmd.Env = append(cmd.Env, "GOTRACEBACK=crash") if early { // Tell testprogcgo to install an early signal handler for SIGABRT cmd.Env = append(cmd.Env, "CGOCATCHPANIC_EARLY_HANDLER=1") } if out, err := cmd.CombinedOutput(); err != nil { t.Errorf("testprogcgo CgoCatchPanic failed: %v\n%s", err, out) } } } func TestCgoLockOSThreadExit(t *testing.T) { switch runtime.GOOS { case "plan9", "windows": t.Skipf("no pthreads on %s", runtime.GOOS) } t.Parallel() testLockOSThreadExit(t, "testprogcgo") } func TestWindowsStackMemoryCgo(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("skipping windows specific test") } testenv.SkipFlaky(t, 22575) o := runTestProg(t, "testprogcgo", "StackMemory") stackUsage, err := strconv.Atoi(o) if err != nil { t.Fatalf("Failed to read stack usage: %v", err) } if expected, got := 100<<10, stackUsage; got > expected { t.Fatalf("expected < %d bytes of memory per thread, got %d", expected, got) } } func TestSigStackSwapping(t *testing.T) { switch runtime.GOOS { case "plan9", "windows": t.Skipf("no sigaltstack on %s", runtime.GOOS) } t.Parallel() got := runTestProg(t, "testprogcgo", "SigStack") want := "OK\n" if got != want { t.Errorf("expected %q got %v", want, got) } } func TestCgoTracebackSigpanic(t *testing.T) { // Test unwinding over a sigpanic in C code without a C // symbolizer. See issue #23576. if runtime.GOOS == "windows" { // On Windows if we get an exception in C code, we let // the Windows exception handler unwind it, rather // than injecting a sigpanic. t.Skip("no sigpanic in C on windows") } if runtime.GOOS == "ios" { testenv.SkipFlaky(t, 59912) } t.Parallel() got := runTestProg(t, "testprogcgo", "TracebackSigpanic") t.Log(got) // We should see the function that calls the C function. want := "main.TracebackSigpanic" if !strings.Contains(got, want) { if runtime.GOOS == "android" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") { testenv.SkipFlaky(t, 58794) } t.Errorf("did not see %q in output", want) } // We shouldn't inject a sigpanic call. (see issue 57698) nowant := "runtime.sigpanic" if strings.Contains(got, nowant) { t.Errorf("unexpectedly saw %q in output", nowant) } // No runtime errors like "runtime: unexpected return pc". nowant = "runtime: " if strings.Contains(got, nowant) { t.Errorf("unexpectedly saw %q in output", nowant) } } func TestCgoPanicCallback(t *testing.T) { t.Parallel() got := runTestProg(t, "testprogcgo", "PanicCallback") t.Log(got) want := "panic: runtime error: invalid memory address or nil pointer dereference" if !strings.Contains(got, want) { t.Errorf("did not see %q in output", want) } want = "panic_callback" if !strings.Contains(got, want) { t.Errorf("did not see %q in output", want) } want = "PanicCallback" if !strings.Contains(got, want) { t.Errorf("did not see %q in output", want) } // No runtime errors like "runtime: unexpected return pc". nowant := "runtime: " if strings.Contains(got, nowant) { t.Errorf("did not see %q in output", want) } } // Test that C code called via cgo can use large Windows thread stacks // and call back in to Go without crashing. See issue #20975. // // See also TestBigStackCallbackSyscall. func TestBigStackCallbackCgo(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("skipping windows specific test") } t.Parallel() got := runTestProg(t, "testprogcgo", "BigStack") want := "OK\n" if got != want { t.Errorf("expected %q got %v", want, got) } } func nextTrace(lines []string) ([]string, []string) { var trace []string for n, line := range lines { if strings.HasPrefix(line, "---") { return trace, lines[n+1:] } fields := strings.Fields(strings.TrimSpace(line)) if len(fields) == 0 { continue } // Last field contains the function name. trace = append(trace, fields[len(fields)-1]) } return nil, nil } func findTrace(text, top string) []string { lines := strings.Split(text, "\n") _, lines = nextTrace(lines) // Skip the header. for len(lines) > 0 { var t []string t, lines = nextTrace(lines) if len(t) == 0 { continue } if t[0] == top { return t } } return nil } func TestSegv(t *testing.T) { switch runtime.GOOS { case "plan9", "windows": t.Skipf("no signals on %s", runtime.GOOS) } for _, test := range []string{"Segv", "SegvInCgo", "TgkillSegv", "TgkillSegvInCgo"} { test := test // The tgkill variants only run on Linux. if runtime.GOOS != "linux" && strings.HasPrefix(test, "Tgkill") { continue } t.Run(test, func(t *testing.T) { if test == "SegvInCgo" && runtime.GOOS == "ios" { testenv.SkipFlaky(t, 59947) // Don't even try, in case it times out. } t.Parallel() prog := "testprog" if strings.HasSuffix(test, "InCgo") { prog = "testprogcgo" } got := runTestProg(t, prog, test) t.Log(got) want := "SIGSEGV" if !strings.Contains(got, want) { if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" && strings.Contains(got, "fatal: morestack on g0") { testenv.SkipFlaky(t, 39457) } t.Errorf("did not see %q in output", want) } // No runtime errors like "runtime: unknown pc". switch runtime.GOOS { case "darwin", "ios", "illumos", "solaris": // Runtime sometimes throws when generating the traceback. testenv.SkipFlaky(t, 49182) case "linux": if runtime.GOARCH == "386" { // Runtime throws when generating a traceback from // a VDSO call via asmcgocall. testenv.SkipFlaky(t, 50504) } } if test == "SegvInCgo" && strings.Contains(got, "unknown pc") { testenv.SkipFlaky(t, 50979) } for _, nowant := range []string{"fatal error: ", "runtime: "} { if strings.Contains(got, nowant) { if runtime.GOOS == "darwin" && strings.Contains(got, "0xb01dfacedebac1e") { // See the comment in signal_darwin_amd64.go. t.Skip("skipping due to Darwin handling of malformed addresses") } t.Errorf("unexpectedly saw %q in output", nowant) } } }) } } func TestAbortInCgo(t *testing.T) { switch runtime.GOOS { case "plan9", "windows": // N.B. On Windows, C abort() causes the program to exit // without going through the runtime at all. t.Skipf("no signals on %s", runtime.GOOS) } t.Parallel() got := runTestProg(t, "testprogcgo", "Abort") t.Log(got) want := "SIGABRT" if !strings.Contains(got, want) { t.Errorf("did not see %q in output", want) } // No runtime errors like "runtime: unknown pc". nowant := "runtime: " if strings.Contains(got, nowant) { t.Errorf("did not see %q in output", want) } } // TestEINTR tests that we handle EINTR correctly. // See issue #20400 and friends. func TestEINTR(t *testing.T) { switch runtime.GOOS { case "plan9", "windows": t.Skipf("no EINTR on %s", runtime.GOOS) case "linux": if runtime.GOARCH == "386" { // On linux-386 the Go signal handler sets // a restorer function that is not preserved // by the C sigaction call in the test, // causing the signal handler to crash when // returning the normal code. The test is not // architecture-specific, so just skip on 386 // rather than doing a complicated workaround. t.Skip("skipping on linux-386; C sigaction does not preserve Go restorer") } } t.Parallel() output := runTestProg(t, "testprogcgo", "EINTR") want := "OK\n" if output != want { t.Fatalf("want %s, got %s\n", want, output) } } // Issue #42207. func TestNeedmDeadlock(t *testing.T) { switch runtime.GOOS { case "plan9", "windows": t.Skipf("no signals on %s", runtime.GOOS) } output := runTestProg(t, "testprogcgo", "NeedmDeadlock") want := "OK\n" if output != want { t.Fatalf("want %s, got %s\n", want, output) } } func TestCgoNoCallback(t *testing.T) { got := runTestProg(t, "testprogcgo", "CgoNoCallback") want := "function marked with #cgo nocallback called back into Go" if !strings.Contains(got, want) { t.Fatalf("did not see %q in output:\n%s", want, got) } } func TestCgoNoEscape(t *testing.T) { got := runTestProg(t, "testprogcgo", "CgoNoEscape") want := "OK\n" if got != want { t.Fatalf("want %s, got %s\n", want, got) } } // Issue #63739. func TestCgoEscapeWithMultiplePointers(t *testing.T) { got := runTestProg(t, "testprogcgo", "CgoEscapeWithMultiplePointers") want := "OK\n" if got != want { t.Fatalf("output is %s; want %s", got, want) } } func TestCgoTracebackGoroutineProfile(t *testing.T) { output := runTestProg(t, "testprogcgo", "GoroutineProfile") want := "OK\n" if output != want { t.Fatalf("want %s, got %s\n", want, output) } } func TestCgoSigfwd(t *testing.T) { t.Parallel() if !goos.IsUnix { t.Skipf("no signals on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "CgoSigfwd", "GO_TEST_CGOSIGFWD=1") if want := "OK\n"; got != want { t.Fatalf("expected %q, but got:\n%s", want, got) } } func TestDestructorCallback(t *testing.T) { t.Parallel() got := runTestProg(t, "testprogcgo", "DestructorCallback") if want := "OK\n"; got != want { t.Errorf("expected %q, but got:\n%s", want, got) } } func TestDestructorCallbackRace(t *testing.T) { // This test requires building with -race, // so it's somewhat slow. if testing.Short() { t.Skip("skipping test in -short mode") } if !platform.RaceDetectorSupported(runtime.GOOS, runtime.GOARCH) { t.Skipf("skipping on %s/%s because race detector not supported", runtime.GOOS, runtime.GOARCH) } t.Parallel() exe, err := buildTestProg(t, "testprogcgo", "-race") if err != nil { t.Fatal(err) } got, err := testenv.CleanCmdEnv(exec.Command(exe, "DestructorCallback")).CombinedOutput() if err != nil { t.Fatal(err) } if want := "OK\n"; string(got) != want { t.Errorf("expected %q, but got:\n%s", want, got) } } func TestEnsureBindM(t *testing.T) { t.Parallel() switch runtime.GOOS { case "windows", "plan9": t.Skipf("skipping bindm test on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "EnsureBindM") want := "OK\n" if got != want { t.Errorf("expected %q, got %v", want, got) } } func TestStackSwitchCallback(t *testing.T) { t.Parallel() switch runtime.GOOS { case "windows", "plan9", "android", "ios", "openbsd": // no getcontext t.Skipf("skipping test on %s", runtime.GOOS) } got := runTestProg(t, "testprogcgo", "StackSwitchCallback") skip := "SKIP\n" if got == skip { t.Skip("skipping on musl/bionic libc") } want := "OK\n" if got != want { t.Errorf("expected %q, got %v", want, got) } } func TestCgoToGoCallGoexit(t *testing.T) { if runtime.GOOS == "plan9" || runtime.GOOS == "windows" { t.Skipf("no pthreads on %s", runtime.GOOS) } output := runTestProg(t, "testprogcgo", "CgoToGoCallGoexit") if !strings.Contains(output, "runtime.Goexit called in a thread that was not created by the Go runtime") { t.Fatalf("output should contain %s, got %s", "runtime.Goexit called in a thread that was not created by the Go runtime", output) } }