diff options
Diffstat (limited to 'src/os/signal/signal_test.go')
-rw-r--r-- | src/os/signal/signal_test.go | 897 |
1 files changed, 897 insertions, 0 deletions
diff --git a/src/os/signal/signal_test.go b/src/os/signal/signal_test.go new file mode 100644 index 0000000..9e5ee8b --- /dev/null +++ b/src/os/signal/signal_test.go @@ -0,0 +1,897 @@ +// 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. + +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package signal + +import ( + "bytes" + "context" + "flag" + "fmt" + "internal/testenv" + "os" + "os/exec" + "runtime" + "runtime/trace" + "strconv" + "sync" + "syscall" + "testing" + "time" +) + +// settleTime is an upper bound on how long we expect signals to take to be +// delivered. Lower values make the test faster, but also flakier — especially +// on heavily loaded systems. +// +// The current value is set based on flakes observed in the Go builders. +var settleTime = 100 * time.Millisecond + +func init() { + if testenv.Builder() == "solaris-amd64-oraclerel" { + // The solaris-amd64-oraclerel builder has been observed to time out in + // TestNohup even with a 250ms settle time. + // + // Use a much longer settle time on that builder to try to suss out whether + // the test is flaky due to builder slowness (which may mean we need a + // longer GO_TEST_TIMEOUT_SCALE) or due to a dropped signal (which may + // instead need a test-skip and upstream bug filed against the Solaris + // kernel). + // + // This constant is chosen so as to make the test as generous as possible + // while still reliably completing within 3 minutes in non-short mode. + // + // See https://golang.org/issue/33174. + settleTime = 11 * time.Second + } else if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { + if scale, err := strconv.Atoi(s); err == nil { + settleTime *= time.Duration(scale) + } + } +} + +func waitSig(t *testing.T, c <-chan os.Signal, sig os.Signal) { + t.Helper() + waitSig1(t, c, sig, false) +} +func waitSigAll(t *testing.T, c <-chan os.Signal, sig os.Signal) { + t.Helper() + waitSig1(t, c, sig, true) +} + +func waitSig1(t *testing.T, c <-chan os.Signal, sig os.Signal, all bool) { + t.Helper() + + // Sleep multiple times to give the kernel more tries to + // deliver the signal. + start := time.Now() + timer := time.NewTimer(settleTime / 10) + defer timer.Stop() + // If the caller notified for all signals on c, filter out SIGURG, + // which is used for runtime preemption and can come at unpredictable times. + // General user code should filter out all unexpected signals instead of just + // SIGURG, but since os/signal is tightly coupled to the runtime it seems + // appropriate to be stricter here. + for time.Since(start) < settleTime { + select { + case s := <-c: + if s == sig { + return + } + if !all || s != syscall.SIGURG { + t.Fatalf("signal was %v, want %v", s, sig) + } + case <-timer.C: + timer.Reset(settleTime / 10) + } + } + t.Fatalf("timeout after %v waiting for %v", settleTime, sig) +} + +// quiesce waits until we can be reasonably confident that all pending signals +// have been delivered by the OS. +func quiesce() { + // The kernel will deliver a signal as a thread returns + // from a syscall. If the only active thread is sleeping, + // and the system is busy, the kernel may not get around + // to waking up a thread to catch the signal. + // We try splitting up the sleep to give the kernel + // many chances to deliver the signal. + start := time.Now() + for time.Since(start) < settleTime { + time.Sleep(settleTime / 10) + } +} + +// Test that basic signal handling works. +func TestSignal(t *testing.T) { + // Ask for SIGHUP + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + defer Stop(c) + + // Send this process a SIGHUP + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSig(t, c, syscall.SIGHUP) + + // Ask for everything we can get. The buffer size has to be + // more than 1, since the runtime might send SIGURG signals. + // Using 10 is arbitrary. + c1 := make(chan os.Signal, 10) + Notify(c1) + + // Send this process a SIGWINCH + t.Logf("sigwinch...") + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + waitSigAll(t, c1, syscall.SIGWINCH) + + // Send two more SIGHUPs, to make sure that + // they get delivered on c1 and that not reading + // from c does not block everything. + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSigAll(t, c1, syscall.SIGHUP) + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSigAll(t, c1, syscall.SIGHUP) + + // The first SIGHUP should be waiting for us on c. + waitSig(t, c, syscall.SIGHUP) +} + +func TestStress(t *testing.T) { + dur := 3 * time.Second + if testing.Short() { + dur = 100 * time.Millisecond + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + + sig := make(chan os.Signal, 1) + Notify(sig, syscall.SIGUSR1) + + go func() { + stop := time.After(dur) + for { + select { + case <-stop: + // Allow enough time for all signals to be delivered before we stop + // listening for them. + quiesce() + Stop(sig) + // According to its documentation, “[w]hen Stop returns, it in + // guaranteed that c will receive no more signals.” So we can safely + // close sig here: if there is a send-after-close race here, that is a + // bug in Stop and we would like to detect it. + close(sig) + return + + default: + syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) + runtime.Gosched() + } + } + }() + + for range sig { + // Receive signals until the sender closes sig. + } +} + +func testCancel(t *testing.T, ignore bool) { + // Ask to be notified on c1 when a SIGWINCH is received. + c1 := make(chan os.Signal, 1) + Notify(c1, syscall.SIGWINCH) + defer Stop(c1) + + // Ask to be notified on c2 when a SIGHUP is received. + c2 := make(chan os.Signal, 1) + Notify(c2, syscall.SIGHUP) + defer Stop(c2) + + // Send this process a SIGWINCH and wait for notification on c1. + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + waitSig(t, c1, syscall.SIGWINCH) + + // Send this process a SIGHUP and wait for notification on c2. + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSig(t, c2, syscall.SIGHUP) + + // Ignore, or reset the signal handlers for, SIGWINCH and SIGHUP. + // Either way, this should undo both calls to Notify above. + if ignore { + Ignore(syscall.SIGWINCH, syscall.SIGHUP) + // Don't bother deferring a call to Reset: it is documented to undo Notify, + // but its documentation says nothing about Ignore, and (as of the time of + // writing) it empirically does not undo an Ignore. + } else { + Reset(syscall.SIGWINCH, syscall.SIGHUP) + } + + // Send this process a SIGWINCH. It should be ignored. + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + + // If ignoring, Send this process a SIGHUP. It should be ignored. + if ignore { + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + } + + quiesce() + + select { + case s := <-c1: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + select { + case s := <-c2: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + // One or both of the signals may have been blocked for this process + // by the calling process. + // Discard any queued signals now to avoid interfering with other tests. + Notify(c1, syscall.SIGWINCH) + Notify(c2, syscall.SIGHUP) + quiesce() +} + +// Test that Reset cancels registration for listed signals on all channels. +func TestReset(t *testing.T) { + testCancel(t, false) +} + +// Test that Ignore cancels registration for listed signals on all channels. +func TestIgnore(t *testing.T) { + testCancel(t, true) +} + +// Test that Ignored correctly detects changes to the ignored status of a signal. +func TestIgnored(t *testing.T) { + // Ask to be notified on SIGWINCH. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGWINCH) + + // If we're being notified, then the signal should not be ignored. + if Ignored(syscall.SIGWINCH) { + t.Errorf("expected SIGWINCH to not be ignored.") + } + Stop(c) + Ignore(syscall.SIGWINCH) + + // We're no longer paying attention to this signal. + if !Ignored(syscall.SIGWINCH) { + t.Errorf("expected SIGWINCH to be ignored when explicitly ignoring it.") + } + + Reset() +} + +var checkSighupIgnored = flag.Bool("check_sighup_ignored", false, "if true, TestDetectNohup will fail if SIGHUP is not ignored.") + +// Test that Ignored(SIGHUP) correctly detects whether it is being run under nohup. +func TestDetectNohup(t *testing.T) { + if *checkSighupIgnored { + if !Ignored(syscall.SIGHUP) { + t.Fatal("SIGHUP is not ignored.") + } else { + t.Log("SIGHUP is ignored.") + } + } else { + defer Reset() + // Ugly: ask for SIGHUP so that child will not have no-hup set + // even if test is running under nohup environment. + // We have no intention of reading from c. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + if out, err := exec.Command(os.Args[0], "-test.run=TestDetectNohup", "-check_sighup_ignored").CombinedOutput(); err == nil { + t.Errorf("ran test with -check_sighup_ignored and it succeeded: expected failure.\nOutput:\n%s", out) + } + Stop(c) + // Again, this time with nohup, assuming we can find it. + _, err := os.Stat("/usr/bin/nohup") + if err != nil { + t.Skip("cannot find nohup; skipping second half of test") + } + Ignore(syscall.SIGHUP) + os.Remove("nohup.out") + out, err := exec.Command("/usr/bin/nohup", os.Args[0], "-test.run=TestDetectNohup", "-check_sighup_ignored").CombinedOutput() + + data, _ := os.ReadFile("nohup.out") + os.Remove("nohup.out") + if err != nil { + t.Errorf("ran test with -check_sighup_ignored under nohup and it failed: expected success.\nError: %v\nOutput:\n%s%s", err, out, data) + } + } +} + +var ( + sendUncaughtSighup = flag.Int("send_uncaught_sighup", 0, "send uncaught SIGHUP during TestStop") + dieFromSighup = flag.Bool("die_from_sighup", false, "wait to die from uncaught SIGHUP") +) + +// Test that Stop cancels the channel's registrations. +func TestStop(t *testing.T) { + sigs := []syscall.Signal{ + syscall.SIGWINCH, + syscall.SIGHUP, + syscall.SIGUSR1, + } + + for _, sig := range sigs { + sig := sig + t.Run(fmt.Sprint(sig), func(t *testing.T) { + // When calling Notify with a specific signal, + // independent signals should not interfere with each other, + // and we end up needing to wait for signals to quiesce a lot. + // Test the three different signals concurrently. + t.Parallel() + + // If the signal is not ignored, send the signal before registering a + // channel to verify the behavior of the default Go handler. + // If it's SIGWINCH or SIGUSR1 we should not see it. + // If it's SIGHUP, maybe we'll die. Let the flag tell us what to do. + mayHaveBlockedSignal := false + if !Ignored(sig) && (sig != syscall.SIGHUP || *sendUncaughtSighup == 1) { + syscall.Kill(syscall.Getpid(), sig) + quiesce() + + // We don't know whether sig is blocked for this process; see + // https://golang.org/issue/38165. Assume that it could be. + mayHaveBlockedSignal = true + } + + // Ask for signal + c := make(chan os.Signal, 1) + Notify(c, sig) + + // Send this process the signal again. + syscall.Kill(syscall.Getpid(), sig) + waitSig(t, c, sig) + + if mayHaveBlockedSignal { + // We may have received a queued initial signal in addition to the one + // that we sent after Notify. If so, waitSig may have observed that + // initial signal instead of the second one, and we may need to wait for + // the second signal to clear. Do that now. + quiesce() + select { + case <-c: + default: + } + } + + // Stop watching for the signal and send it again. + // If it's SIGHUP, maybe we'll die. Let the flag tell us what to do. + Stop(c) + if sig != syscall.SIGHUP || *sendUncaughtSighup == 2 { + syscall.Kill(syscall.Getpid(), sig) + quiesce() + + select { + case s := <-c: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + // If we're going to receive a signal, it has almost certainly been + // received by now. However, it may have been blocked for this process — + // we don't know. Explicitly unblock it and wait for it to clear now. + Notify(c, sig) + quiesce() + Stop(c) + } + }) + } +} + +// Test that when run under nohup, an uncaught SIGHUP does not kill the program. +func TestNohup(t *testing.T) { + // Ugly: ask for SIGHUP so that child will not have no-hup set + // even if test is running under nohup environment. + // We have no intention of reading from c. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + + // When run without nohup, the test should crash on an uncaught SIGHUP. + // When run under nohup, the test should ignore uncaught SIGHUPs, + // because the runtime is not supposed to be listening for them. + // Either way, TestStop should still be able to catch them when it wants them + // and then when it stops wanting them, the original behavior should resume. + // + // send_uncaught_sighup=1 sends the SIGHUP before starting to listen for SIGHUPs. + // send_uncaught_sighup=2 sends the SIGHUP after no longer listening for SIGHUPs. + // + // Both should fail without nohup and succeed with nohup. + + var subTimeout time.Duration + + var wg sync.WaitGroup + wg.Add(2) + if deadline, ok := t.Deadline(); ok { + subTimeout = time.Until(deadline) + subTimeout -= subTimeout / 10 // Leave 10% headroom for propagating output. + } + for i := 1; i <= 2; i++ { + i := i + go t.Run(fmt.Sprintf("uncaught-%d", i), func(t *testing.T) { + defer wg.Done() + + args := []string{ + "-test.v", + "-test.run=TestStop", + "-send_uncaught_sighup=" + strconv.Itoa(i), + "-die_from_sighup", + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := exec.Command(os.Args[0], args...).CombinedOutput() + + if err == nil { + t.Errorf("ran test with -send_uncaught_sighup=%d and it succeeded: expected failure.\nOutput:\n%s", i, out) + } else { + t.Logf("test with -send_uncaught_sighup=%d failed as expected.\nError: %v\nOutput:\n%s", i, err, out) + } + }) + } + wg.Wait() + + Stop(c) + + // Skip the nohup test below when running in tmux on darwin, since nohup + // doesn't work correctly there. See issue #5135. + if runtime.GOOS == "darwin" && os.Getenv("TMUX") != "" { + t.Skip("Skipping nohup test due to running in tmux on darwin") + } + + // Again, this time with nohup, assuming we can find it. + _, err := exec.LookPath("nohup") + if err != nil { + t.Skip("cannot find nohup; skipping second half of test") + } + + wg.Add(2) + if deadline, ok := t.Deadline(); ok { + subTimeout = time.Until(deadline) + subTimeout -= subTimeout / 10 // Leave 10% headroom for propagating output. + } + for i := 1; i <= 2; i++ { + i := i + go t.Run(fmt.Sprintf("nohup-%d", i), func(t *testing.T) { + defer wg.Done() + + // POSIX specifies that nohup writes to a file named nohup.out if standard + // output is a terminal. However, for an exec.Command, standard output is + // not a terminal — so we don't need to read or remove that file (and, + // indeed, cannot even create it if the current user is unable to write to + // GOROOT/src, such as when GOROOT is installed and owned by root). + + args := []string{ + os.Args[0], + "-test.v", + "-test.run=TestStop", + "-send_uncaught_sighup=" + strconv.Itoa(i), + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := exec.Command("nohup", args...).CombinedOutput() + + if err != nil { + t.Errorf("ran test with -send_uncaught_sighup=%d under nohup and it failed: expected success.\nError: %v\nOutput:\n%s", i, err, out) + } else { + t.Logf("ran test with -send_uncaught_sighup=%d under nohup.\nOutput:\n%s", i, out) + } + }) + } + wg.Wait() +} + +// Test that SIGCONT works (issue 8953). +func TestSIGCONT(t *testing.T) { + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGCONT) + defer Stop(c) + syscall.Kill(syscall.Getpid(), syscall.SIGCONT) + waitSig(t, c, syscall.SIGCONT) +} + +// Test race between stopping and receiving a signal (issue 14571). +func TestAtomicStop(t *testing.T) { + if os.Getenv("GO_TEST_ATOMIC_STOP") != "" { + atomicStopTestProgram(t) + t.Fatal("atomicStopTestProgram returned") + } + + testenv.MustHaveExec(t) + + // Call Notify for SIGINT before starting the child process. + // That ensures that SIGINT is not ignored for the child. + // This is necessary because if SIGINT is ignored when a + // Go program starts, then it remains ignored, and closing + // the last notification channel for SIGINT will switch it + // back to being ignored. In that case the assumption of + // atomicStopTestProgram, that it will either die from SIGINT + // or have it be reported, breaks down, as there is a third + // option: SIGINT might be ignored. + cs := make(chan os.Signal, 1) + Notify(cs, syscall.SIGINT) + defer Stop(cs) + + const execs = 10 + for i := 0; i < execs; i++ { + timeout := "0" + if deadline, ok := t.Deadline(); ok { + timeout = time.Until(deadline).String() + } + cmd := exec.Command(os.Args[0], "-test.run=TestAtomicStop", "-test.timeout="+timeout) + cmd.Env = append(os.Environ(), "GO_TEST_ATOMIC_STOP=1") + out, err := cmd.CombinedOutput() + if err == nil { + if len(out) > 0 { + t.Logf("iteration %d: output %s", i, out) + } + } else { + t.Logf("iteration %d: exit status %q: output: %s", i, err, out) + } + + lost := bytes.Contains(out, []byte("lost signal")) + if lost { + t.Errorf("iteration %d: lost signal", i) + } + + // The program should either die due to SIGINT, + // or exit with success without printing "lost signal". + if err == nil { + if len(out) > 0 && !lost { + t.Errorf("iteration %d: unexpected output", i) + } + } else { + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("iteration %d: error (%v) has type %T; expected exec.ExitError", i, err, err) + } else if ws, ok := ee.Sys().(syscall.WaitStatus); !ok { + t.Errorf("iteration %d: error.Sys (%v) has type %T; expected syscall.WaitStatus", i, ee.Sys(), ee.Sys()) + } else if !ws.Signaled() || ws.Signal() != syscall.SIGINT { + t.Errorf("iteration %d: got exit status %v; expected SIGINT", i, ee) + } + } + } +} + +// atomicStopTestProgram is run in a subprocess by TestAtomicStop. +// It tries to trigger a signal delivery race. This function should +// either catch a signal or die from it. +func atomicStopTestProgram(t *testing.T) { + // This test won't work if SIGINT is ignored here. + if Ignored(syscall.SIGINT) { + fmt.Println("SIGINT is ignored") + os.Exit(1) + } + + const tries = 10 + + timeout := 2 * time.Second + if deadline, ok := t.Deadline(); ok { + // Give each try an equal slice of the deadline, with one slice to spare for + // cleanup. + timeout = time.Until(deadline) / (tries + 1) + } + + pid := syscall.Getpid() + printed := false + for i := 0; i < tries; i++ { + cs := make(chan os.Signal, 1) + Notify(cs, syscall.SIGINT) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + Stop(cs) + }() + + syscall.Kill(pid, syscall.SIGINT) + + // At this point we should either die from SIGINT or + // get a notification on cs. If neither happens, we + // dropped the signal. It is given 2 seconds to + // deliver, as needed for gccgo on some loaded test systems. + + select { + case <-cs: + case <-time.After(timeout): + if !printed { + fmt.Print("lost signal on tries:") + printed = true + } + fmt.Printf(" %d", i) + } + + wg.Wait() + } + if printed { + fmt.Print("\n") + } + + os.Exit(0) +} + +func TestTime(t *testing.T) { + // Test that signal works fine when we are in a call to get time, + // which on some platforms is using VDSO. See issue #34391. + dur := 3 * time.Second + if testing.Short() { + dur = 100 * time.Millisecond + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + + sig := make(chan os.Signal, 1) + Notify(sig, syscall.SIGUSR1) + + stop := make(chan struct{}) + go func() { + for { + select { + case <-stop: + // Allow enough time for all signals to be delivered before we stop + // listening for them. + quiesce() + Stop(sig) + // According to its documentation, “[w]hen Stop returns, it in + // guaranteed that c will receive no more signals.” So we can safely + // close sig here: if there is a send-after-close race, that is a bug in + // Stop and we would like to detect it. + close(sig) + return + + default: + syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) + runtime.Gosched() + } + } + }() + + done := make(chan struct{}) + go func() { + for range sig { + // Receive signals until the sender closes sig. + } + close(done) + }() + + t0 := time.Now() + for t1 := t0; t1.Sub(t0) < dur; t1 = time.Now() { + } // hammering on getting time + + close(stop) + <-done +} + +var ( + checkNotifyContext = flag.Bool("check_notify_ctx", false, "if true, TestNotifyContext will fail if SIGINT is not received.") + ctxNotifyTimes = flag.Int("ctx_notify_times", 1, "number of times a SIGINT signal should be received") +) + +func TestNotifyContextNotifications(t *testing.T) { + if *checkNotifyContext { + ctx, _ := NotifyContext(context.Background(), syscall.SIGINT) + // We want to make sure not to be calling Stop() internally on NotifyContext() when processing a received signal. + // Being able to wait for a number of received system signals allows us to do so. + var wg sync.WaitGroup + n := *ctxNotifyTimes + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + syscall.Kill(syscall.Getpid(), syscall.SIGINT) + wg.Done() + }() + } + wg.Wait() + <-ctx.Done() + fmt.Print("received SIGINT") + // Sleep to give time to simultaneous signals to reach the process. + // These signals must be ignored given stop() is not called on this code. + // We want to guarantee a SIGINT doesn't cause a premature termination of the program. + time.Sleep(settleTime) + return + } + + t.Parallel() + testCases := []struct { + name string + n int // number of times a SIGINT should be notified. + }{ + {"once", 1}, + {"multiple", 10}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var subTimeout time.Duration + if deadline, ok := t.Deadline(); ok { + subTimeout := time.Until(deadline) + subTimeout -= subTimeout / 10 // Leave 10% headroom for cleaning up subprocess. + } + + args := []string{ + "-test.v", + "-test.run=TestNotifyContextNotifications$", + "-check_notify_ctx", + fmt.Sprintf("-ctx_notify_times=%d", tc.n), + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := exec.Command(os.Args[0], args...).CombinedOutput() + if err != nil { + t.Errorf("ran test with -check_notify_ctx_notification and it failed with %v.\nOutput:\n%s", err, out) + } + if want := []byte("received SIGINT"); !bytes.Contains(out, want) { + t.Errorf("got %q, wanted %q", out, want) + } + }) + } +} + +func TestNotifyContextStop(t *testing.T) { + Ignore(syscall.SIGHUP) + if !Ignored(syscall.SIGHUP) { + t.Errorf("expected SIGHUP to be ignored when explicitly ignoring it.") + } + + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGHUP) + defer stop() + + // If we're being notified, then the signal should not be ignored. + if Ignored(syscall.SIGHUP) { + t.Errorf("expected SIGHUP to not be ignored.") + } + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [hangup])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, wanted %q", got, want) + } + + stop() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for context to be done after calling stop") + } +} + +func TestNotifyContextCancelParent(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + cancelParent() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for parent context to be canceled") + } +} + +func TestNotifyContextPrematureCancelParent(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + + cancelParent() // Prematurely cancel context before calling NotifyContext. + c, stop := NotifyContext(parent, syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for parent context to be canceled") + } +} + +func TestNotifyContextSimultaneousStop(t *testing.T) { + c, stop := NotifyContext(context.Background(), syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + var wg sync.WaitGroup + n := 10 + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + stop() + wg.Done() + }() + } + wg.Wait() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("expected context to be canceled") + } +} + +func TestNotifyContextStringer(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) + defer stop() + + want := `signal.NotifyContext(context.Background.WithCancel, [hangup interrupt terminated])` + if got := fmt.Sprint(c); got != want { + t.Errorf("c.String() = %q, want %q", got, want) + } +} + +// #44193 test signal handling while stopping and starting the world. +func TestSignalTrace(t *testing.T) { + done := make(chan struct{}) + quit := make(chan struct{}) + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + + // Source and sink for signals busy loop unsynchronized with + // trace starts and stops. We are ultimately validating that + // signals and runtime.(stop|start)TheWorldGC are compatible. + go func() { + defer close(done) + defer Stop(c) + pid := syscall.Getpid() + for { + select { + case <-quit: + return + default: + syscall.Kill(pid, syscall.SIGHUP) + } + waitSig(t, c, syscall.SIGHUP) + } + }() + + for i := 0; i < 100; i++ { + buf := new(bytes.Buffer) + if err := trace.Start(buf); err != nil { + t.Fatalf("[%d] failed to start tracing: %v", i, err) + } + time.After(1 * time.Microsecond) + trace.Stop() + size := buf.Len() + if size == 0 { + t.Fatalf("[%d] trace is empty", i) + } + } + close(quit) + <-done +} |