mirror of https://github.com/golang/go.git
testing/synctest: add Test
Add a synctest.Test function, superseding the experimental synctest.Run function. Promote the testing/synctest package out of experimental status. For #67434 For #73567 Change-Id: I3c5ba030860d90fe2ddb517a2f3536efd60181a9 Reviewed-on: https://go-review.googlesource.com/c/go/+/671961 Auto-Submit: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Pratt <mpratt@google.com>
This commit is contained in:
parent
609197b406
commit
49a660e22c
|
|
@ -0,0 +1,2 @@
|
|||
pkg testing/synctest, func Test(*testing.T, func(*testing.T)) #67434
|
||||
pkg testing/synctest, func Wait() #67434
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
### New testing/synctest package
|
||||
|
||||
The new [testing/synctest](/pkg/testing/synctest) package
|
||||
provides support for testing concurrent code.
|
||||
|
||||
The [synctest.Test] function runs a test function in an isolated
|
||||
"bubble". Within the bubble, [time](/pkg/time) package functions
|
||||
operate on a fake clock.
|
||||
|
||||
The [synctest.Wait] function waits for all goroutines in the
|
||||
current bubble to block.
|
||||
|
|
@ -0,0 +1 @@
|
|||
<!-- testing/synctest -->
|
||||
|
|
@ -136,8 +136,7 @@ var depsRules = `
|
|||
unicode !< path;
|
||||
|
||||
RUNTIME
|
||||
< internal/synctest
|
||||
< testing/synctest;
|
||||
< internal/synctest;
|
||||
|
||||
# SYSCALL is RUNTIME plus the packages necessary for basic system calls.
|
||||
RUNTIME, unicode/utf8, unicode/utf16, internal/synctest
|
||||
|
|
@ -713,6 +712,9 @@ var depsRules = `
|
|||
FMT
|
||||
< internal/txtar;
|
||||
|
||||
internal/synctest, testing
|
||||
< testing/synctest;
|
||||
|
||||
testing
|
||||
< internal/testhash;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
// Copyright 2025 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 goexperiment.synctest
|
||||
|
||||
package synctest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This example demonstrates testing the context.AfterFunc function.
|
||||
//
|
||||
// AfterFunc registers a function to execute in a new goroutine
|
||||
// after a context is canceled.
|
||||
//
|
||||
// The test verifies that the function is not run before the context is canceled,
|
||||
// and is run after the context is canceled.
|
||||
func Example_contextAfterFunc() {
|
||||
synctest.Run(func() {
|
||||
// Create a context.Context which can be canceled.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// context.AfterFunc registers a function to be called
|
||||
// when a context is canceled.
|
||||
afterFuncCalled := false
|
||||
context.AfterFunc(ctx, func() {
|
||||
afterFuncCalled = true
|
||||
})
|
||||
|
||||
// The context has not been canceled, so the AfterFunc is not called.
|
||||
synctest.Wait()
|
||||
fmt.Printf("before context is canceled: afterFuncCalled=%v\n", afterFuncCalled)
|
||||
|
||||
// Cancel the context and wait for the AfterFunc to finish executing.
|
||||
// Verify that the AfterFunc ran.
|
||||
cancel()
|
||||
synctest.Wait()
|
||||
fmt.Printf("after context is canceled: afterFuncCalled=%v\n", afterFuncCalled)
|
||||
|
||||
// Output:
|
||||
// before context is canceled: afterFuncCalled=false
|
||||
// after context is canceled: afterFuncCalled=true
|
||||
})
|
||||
}
|
||||
|
||||
// This example demonstrates testing the context.WithTimeout function.
|
||||
//
|
||||
// WithTimeout creates a context which is canceled after a timeout.
|
||||
//
|
||||
// The test verifies that the context is not canceled before the timeout expires,
|
||||
// and is canceled after the timeout expires.
|
||||
func Example_contextWithTimeout() {
|
||||
synctest.Run(func() {
|
||||
// Create a context.Context which is canceled after a timeout.
|
||||
const timeout = 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Wait just less than the timeout.
|
||||
time.Sleep(timeout - time.Nanosecond)
|
||||
synctest.Wait()
|
||||
fmt.Printf("before timeout: ctx.Err() = %v\n", ctx.Err())
|
||||
|
||||
// Wait the rest of the way until the timeout.
|
||||
time.Sleep(time.Nanosecond)
|
||||
synctest.Wait()
|
||||
fmt.Printf("after timeout: ctx.Err() = %v\n", ctx.Err())
|
||||
|
||||
// Output:
|
||||
// before timeout: ctx.Err() = <nil>
|
||||
// after timeout: ctx.Err() = context deadline exceeded
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
// Copyright 2024 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 synctest_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Keep the following tests in sync with the package documentation.
|
||||
|
||||
func TestTime(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
start := time.Now() // always midnight UTC 2001-01-01
|
||||
go func() {
|
||||
time.Sleep(1 * time.Nanosecond)
|
||||
t.Log(time.Since(start)) // always logs "1ns"
|
||||
}()
|
||||
time.Sleep(2 * time.Nanosecond) // the AfterFunc will run before this Sleep returns
|
||||
t.Log(time.Since(start)) // always logs "2ns"
|
||||
})
|
||||
}
|
||||
|
||||
func TestWait(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
done := false
|
||||
go func() {
|
||||
done = true
|
||||
}()
|
||||
// Wait will block until the goroutine above has finished.
|
||||
synctest.Wait()
|
||||
t.Log(done) // always logs "true"
|
||||
})
|
||||
}
|
||||
|
||||
func TestContextAfterFunc(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
// Create a context.Context which can be canceled.
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
// context.AfterFunc registers a function to be called
|
||||
// when a context is canceled.
|
||||
afterFuncCalled := false
|
||||
context.AfterFunc(ctx, func() {
|
||||
afterFuncCalled = true
|
||||
})
|
||||
|
||||
// The context has not been canceled, so the AfterFunc is not called.
|
||||
synctest.Wait()
|
||||
if afterFuncCalled {
|
||||
t.Fatalf("before context is canceled: AfterFunc called")
|
||||
}
|
||||
|
||||
// Cancel the context and wait for the AfterFunc to finish executing.
|
||||
// Verify that the AfterFunc ran.
|
||||
cancel()
|
||||
synctest.Wait()
|
||||
if !afterFuncCalled {
|
||||
t.Fatalf("before context is canceled: AfterFunc not called")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContextWithTimeout(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
// Create a context.Context which is canceled after a timeout.
|
||||
const timeout = 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(t.Context(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Wait just less than the timeout.
|
||||
time.Sleep(timeout - time.Nanosecond)
|
||||
synctest.Wait()
|
||||
if err := ctx.Err(); err != nil {
|
||||
t.Fatalf("before timeout: ctx.Err() = %v, want nil\n", err)
|
||||
}
|
||||
|
||||
// Wait the rest of the way until the timeout.
|
||||
time.Sleep(time.Nanosecond)
|
||||
synctest.Wait()
|
||||
if err := ctx.Err(); err != context.DeadlineExceeded {
|
||||
t.Fatalf("after timeout: ctx.Err() = %v, want DeadlineExceeded\n", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPTransport100Continue(t *testing.T) {
|
||||
synctest.Test(t, func(*testing.T) {
|
||||
// Create an in-process fake network connection.
|
||||
// We cannot use a loopback network connection for this test,
|
||||
// because goroutines blocked on network I/O prevent a synctest
|
||||
// bubble from becoming idle.
|
||||
srvConn, cliConn := net.Pipe()
|
||||
defer cliConn.Close()
|
||||
defer srvConn.Close()
|
||||
|
||||
tr := &http.Transport{
|
||||
// Use the fake network connection created above.
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return cliConn, nil
|
||||
},
|
||||
// Enable "Expect: 100-continue" handling.
|
||||
ExpectContinueTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// Send a request with the "Expect: 100-continue" header set.
|
||||
// Send it in a new goroutine, since it won't complete until the end of the test.
|
||||
body := "request body"
|
||||
go func() {
|
||||
req, _ := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body))
|
||||
req.Header.Set("Expect", "100-continue")
|
||||
resp, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Errorf("RoundTrip: unexpected error %v\n", err)
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Read the request headers sent by the client.
|
||||
req, err := http.ReadRequest(bufio.NewReader(srvConn))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadRequest: %v\n", err)
|
||||
}
|
||||
|
||||
// Start a new goroutine copying the body sent by the client into a buffer.
|
||||
// Wait for all goroutines in the bubble to block and verify that we haven't
|
||||
// read anything from the client yet.
|
||||
var gotBody bytes.Buffer
|
||||
go io.Copy(&gotBody, req.Body)
|
||||
synctest.Wait()
|
||||
if got, want := gotBody.String(), ""; got != want {
|
||||
t.Fatalf("before sending 100 Continue, read body: %q, want %q\n", got, want)
|
||||
}
|
||||
|
||||
// Write a "100 Continue" response to the client and verify that
|
||||
// it sends the request body.
|
||||
srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n"))
|
||||
synctest.Wait()
|
||||
if got, want := gotBody.String(), body; got != want {
|
||||
t.Fatalf("after sending 100 Continue, read body: %q, want %q\n", got, want)
|
||||
}
|
||||
|
||||
// Finish up by sending the "200 OK" response to conclude the request.
|
||||
srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
// We started several goroutines during the test.
|
||||
// The synctest.Test call will wait for all of them to exit before returning.
|
||||
})
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
// Copyright 2025 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 goexperiment.synctest
|
||||
|
||||
package synctest_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This example demonstrates testing [http.Transport]'s 100 Continue handling.
|
||||
//
|
||||
// An HTTP client sending a request can include an "Expect: 100-continue" header
|
||||
// to tell the server that the client has additional data to send.
|
||||
// The server may then respond with an 100 Continue information response
|
||||
// to request the data, or some other status to tell the client the data is not needed.
|
||||
// For example, a client uploading a large file might use this feature to confirm
|
||||
// that the server is willing to accept the file before sending it.
|
||||
//
|
||||
// This test confirms that when sending an "Expect: 100-continue" header
|
||||
// the HTTP client does not send a request's content before the server requests it,
|
||||
// and that it does send the content after receiving a 100 Continue response.
|
||||
func Example_httpTransport100Continue() {
|
||||
synctest.Run(func() {
|
||||
// Create an in-process fake network connection.
|
||||
// We cannot use a loopback network connection for this test,
|
||||
// because goroutines blocked on network I/O prevent a synctest
|
||||
// bubble from becoming idle.
|
||||
srvConn, cliConn := net.Pipe()
|
||||
defer cliConn.Close()
|
||||
defer srvConn.Close()
|
||||
|
||||
tr := &http.Transport{
|
||||
// Use the fake network connection created above.
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return cliConn, nil
|
||||
},
|
||||
// Enable "Expect: 100-continue" handling.
|
||||
ExpectContinueTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// Send a request with the "Expect: 100-continue" header set.
|
||||
// Send it in a new goroutine, since it won't complete until the end of the test.
|
||||
body := "request body"
|
||||
go func() {
|
||||
req, err := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Header.Set("Expect", "100-continue")
|
||||
resp, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
fmt.Printf("RoundTrip: unexpected error %v\n", err)
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Read the request headers sent by the client.
|
||||
req, err := http.ReadRequest(bufio.NewReader(srvConn))
|
||||
if err != nil {
|
||||
fmt.Printf("ReadRequest: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start a new goroutine copying the body sent by the client into a buffer.
|
||||
// Wait for all goroutines in the bubble to block and verify that we haven't
|
||||
// read anything from the client yet.
|
||||
var gotBody bytes.Buffer
|
||||
go io.Copy(&gotBody, req.Body)
|
||||
synctest.Wait()
|
||||
fmt.Printf("before sending 100 Continue, read body: %q\n", gotBody.String())
|
||||
|
||||
// Write a "100 Continue" response to the client and verify that
|
||||
// it sends the request body.
|
||||
srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n"))
|
||||
synctest.Wait()
|
||||
fmt.Printf("after sending 100 Continue, read body: %q\n", gotBody.String())
|
||||
|
||||
// Finish up by sending the "200 OK" response to conclude the request.
|
||||
srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
// We started several goroutines during the test.
|
||||
// The synctest.Run call will wait for all of them to exit before returning.
|
||||
})
|
||||
|
||||
// Output:
|
||||
// before sending 100 Continue, read body: ""
|
||||
// after sending 100 Continue, read body: "request body"
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2025 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 goexperiment.synctest
|
||||
|
||||
package synctest
|
||||
|
||||
import "internal/synctest"
|
||||
|
||||
// Run is deprecated.
|
||||
//
|
||||
// Deprecated: Use Test instead.
|
||||
func Run(f func()) {
|
||||
synctest.Run(f)
|
||||
}
|
||||
|
|
@ -2,69 +2,273 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build goexperiment.synctest
|
||||
|
||||
// Package synctest provides support for testing concurrent code.
|
||||
//
|
||||
// This package only exists when using Go compiled with GOEXPERIMENT=synctest.
|
||||
// It is experimental, and not subject to the Go 1 compatibility promise.
|
||||
// The [Test] function runs a function in an isolated "bubble".
|
||||
// Any goroutines started within the bubble are also part of the bubble.
|
||||
//
|
||||
// # Time
|
||||
//
|
||||
// Within a bubble, the [time] package uses a fake clock.
|
||||
// Each bubble has its own clock.
|
||||
// The initial time is midnight UTC 2000-01-01.
|
||||
//
|
||||
// Time in a bubble only advances when every goroutine in the
|
||||
// bubble is durably blocked.
|
||||
// See below for the exact definition of "durably blocked".
|
||||
//
|
||||
// For example, this test runs immediately rather than taking
|
||||
// two seconds:
|
||||
//
|
||||
// func TestTime(t *testing.T) {
|
||||
// synctest.Test(t, func(t *testing.T) {
|
||||
// start := time.Now() // always midnight UTC 2001-01-01
|
||||
// go func() {
|
||||
// time.Sleep(1 * time.Nanosecond)
|
||||
// t.Log(time.Since(start)) // always logs "1ns"
|
||||
// }()
|
||||
// time.Sleep(2 * time.Nanosecond) // the goroutine above will run before this Sleep returns
|
||||
// t.Log(time.Since(start)) // always logs "2ns"
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// Time stops advancing when the root goroutine of the bubble exits.
|
||||
//
|
||||
// # Blocking
|
||||
//
|
||||
// A goroutine in a bubble is "durably blocked" when it is blocked
|
||||
// and can only be unblocked by another goroutine in the same bubble.
|
||||
// A goroutine which can be unblocked by an event from outside its
|
||||
// bubble is not durably blocked.
|
||||
//
|
||||
// The [Wait] function blocks until all other goroutines in the
|
||||
// bubble are durably blocked.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// func TestWait(t *testing.T) {
|
||||
// synctest.Test(t, func(t *testing.T) {
|
||||
// done := false
|
||||
// go func() {
|
||||
// done = true
|
||||
// }()
|
||||
// // Wait will block until the goroutine above has finished.
|
||||
// synctest.Wait()
|
||||
// t.Log(done) // always logs "true"
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// When every goroutine in a bubble is durably blocked:
|
||||
//
|
||||
// - [Wait] returns, if it has been called.
|
||||
// - Otherwise, time advances to the next time that will
|
||||
// unblock at least one goroutine, if there is such a time
|
||||
// and the root goroutine of the bubble has not exited.
|
||||
// - Otherwise, there is a deadlock and [Test] panics.
|
||||
//
|
||||
// The following operations durably block a goroutine:
|
||||
//
|
||||
// - a blocking send or receive on a channel created within the bubble
|
||||
// - a blocking select statement where every case is a channel created
|
||||
// within the bubble
|
||||
// - [sync.Cond.Wait]
|
||||
// - [sync.WaitGroup.Wait]
|
||||
// - [time.Sleep]
|
||||
//
|
||||
// Locking a [sync.Mutex] or [sync.RWMutex] is not durably blocking.
|
||||
//
|
||||
// # Isolation
|
||||
//
|
||||
// A channel, [time.Timer], or [time.Ticker] created within a bubble
|
||||
// is associated with it. Operating on a bubbled channel, timer, or
|
||||
// ticker from outside the bubble panics.
|
||||
//
|
||||
// # Example: Context.AfterFunc
|
||||
//
|
||||
// This example demonstrates testing the [context.AfterFunc] function.
|
||||
//
|
||||
// AfterFunc registers a function to execute in a new goroutine
|
||||
// after a context is canceled.
|
||||
//
|
||||
// The test verifies that the function is not run before the context is canceled,
|
||||
// and is run after the context is canceled.
|
||||
//
|
||||
// func TestContextAfterFunc(t *testing.T) {
|
||||
// synctest.Test(t, func(t *testing.T) {
|
||||
// // Create a context.Context which can be canceled.
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
//
|
||||
// // context.AfterFunc registers a function to be called
|
||||
// // when a context is canceled.
|
||||
// afterFuncCalled := false
|
||||
// context.AfterFunc(ctx, func() {
|
||||
// afterFuncCalled = true
|
||||
// })
|
||||
//
|
||||
// // The context has not been canceled, so the AfterFunc is not called.
|
||||
// synctest.Wait()
|
||||
// if afterFuncCalled {
|
||||
// t.Fatalf("before context is canceled: AfterFunc called")
|
||||
// }
|
||||
//
|
||||
// // Cancel the context and wait for the AfterFunc to finish executing.
|
||||
// // Verify that the AfterFunc ran.
|
||||
// cancel()
|
||||
// synctest.Wait()
|
||||
// if !afterFuncCalled {
|
||||
// t.Fatalf("before context is canceled: AfterFunc not called")
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// # Example: Context.WithTimeout
|
||||
//
|
||||
// This example demonstrates testing the [context.WithTimeout] function.
|
||||
//
|
||||
// WithTimeout creates a context which is canceled after a timeout.
|
||||
//
|
||||
// The test verifies that the context is not canceled before the timeout expires,
|
||||
// and is canceled after the timeout expires.
|
||||
//
|
||||
// func TestContextWithTimeout(t *testing.T) {
|
||||
// synctest.Test(t, func(t *testing.T) {
|
||||
// // Create a context.Context which is canceled after a timeout.
|
||||
// const timeout = 5 * time.Second
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), timeout)
|
||||
// defer cancel()
|
||||
//
|
||||
// // Wait just less than the timeout.
|
||||
// time.Sleep(timeout - time.Nanosecond)
|
||||
// synctest.Wait()
|
||||
// if err := ctx.Err(); err != nil {
|
||||
// t.Fatalf("before timeout: ctx.Err() = %v, want nil\n", err)
|
||||
// }
|
||||
//
|
||||
// // Wait the rest of the way until the timeout.
|
||||
// time.Sleep(time.Nanosecond)
|
||||
// synctest.Wait()
|
||||
// if err := ctx.Err(); err != context.DeadlineExceeded {
|
||||
// t.Fatalf("after timeout: ctx.Err() = %v, want DeadlineExceeded\n", err)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// # Example: HTTP 100 Continue
|
||||
//
|
||||
// This example demonstrates testing [http.Transport]'s 100 Continue handling.
|
||||
//
|
||||
// An HTTP client sending a request can include an "Expect: 100-continue" header
|
||||
// to tell the server that the client has additional data to send.
|
||||
// The server may then respond with an 100 Continue information response
|
||||
// to request the data, or some other status to tell the client the data is not needed.
|
||||
// For example, a client uploading a large file might use this feature to confirm
|
||||
// that the server is willing to accept the file before sending it.
|
||||
//
|
||||
// This test confirms that when sending an "Expect: 100-continue" header
|
||||
// the HTTP client does not send a request's content before the server requests it,
|
||||
// and that it does send the content after receiving a 100 Continue response.
|
||||
//
|
||||
// func TestHTTPTransport100Continue(t *testing.T) {
|
||||
// synctest.Test(t, func(*testing.T) {
|
||||
// // Create an in-process fake network connection.
|
||||
// // We cannot use a loopback network connection for this test,
|
||||
// // because goroutines blocked on network I/O prevent a synctest
|
||||
// // bubble from becoming idle.
|
||||
// srvConn, cliConn := net.Pipe()
|
||||
// defer cliConn.Close()
|
||||
// defer srvConn.Close()
|
||||
//
|
||||
// tr := &http.Transport{
|
||||
// // Use the fake network connection created above.
|
||||
// DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
// return cliConn, nil
|
||||
// },
|
||||
// // Enable "Expect: 100-continue" handling.
|
||||
// ExpectContinueTimeout: 5 * time.Second,
|
||||
// }
|
||||
//
|
||||
// // Send a request with the "Expect: 100-continue" header set.
|
||||
// // Send it in a new goroutine, since it won't complete until the end of the test.
|
||||
// body := "request body"
|
||||
// go func() {
|
||||
// req, _ := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body))
|
||||
// req.Header.Set("Expect", "100-continue")
|
||||
// resp, err := tr.RoundTrip(req)
|
||||
// if err != nil {
|
||||
// t.Errorf("RoundTrip: unexpected error %v\n", err)
|
||||
// } else {
|
||||
// resp.Body.Close()
|
||||
// }
|
||||
// }()
|
||||
//
|
||||
// // Read the request headers sent by the client.
|
||||
// req, err := http.ReadRequest(bufio.NewReader(srvConn))
|
||||
// if err != nil {
|
||||
// t.Fatalf("ReadRequest: %v\n", err)
|
||||
// }
|
||||
//
|
||||
// // Start a new goroutine copying the body sent by the client into a buffer.
|
||||
// // Wait for all goroutines in the bubble to block and verify that we haven't
|
||||
// // read anything from the client yet.
|
||||
// var gotBody bytes.Buffer
|
||||
// go io.Copy(&gotBody, req.Body)
|
||||
// synctest.Wait()
|
||||
// if got, want := gotBody.String(), ""; got != want {
|
||||
// t.Fatalf("before sending 100 Continue, read body: %q, want %q\n", got, want)
|
||||
// }
|
||||
//
|
||||
// // Write a "100 Continue" response to the client and verify that
|
||||
// // it sends the request body.
|
||||
// srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n"))
|
||||
// synctest.Wait()
|
||||
// if got, want := gotBody.String(), body; got != want {
|
||||
// t.Fatalf("after sending 100 Continue, read body: %q, want %q\n", got, want)
|
||||
// }
|
||||
//
|
||||
// // Finish up by sending the "200 OK" response to conclude the request.
|
||||
// srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
//
|
||||
// // We started several goroutines during the test.
|
||||
// // The synctest.Test call will wait for all of them to exit before returning.
|
||||
// })
|
||||
// }
|
||||
package synctest
|
||||
|
||||
import (
|
||||
"internal/synctest"
|
||||
"testing"
|
||||
_ "unsafe" // for linkname
|
||||
)
|
||||
|
||||
// Run executes f in a new goroutine.
|
||||
// Test executes f in a new bubble.
|
||||
//
|
||||
// The new goroutine and any goroutines transitively started by it form
|
||||
// an isolated "bubble".
|
||||
// Run waits for all goroutines in the bubble to exit before returning.
|
||||
// Test waits for all goroutines in the bubble to exit before returning.
|
||||
// If the goroutines in the bubble become deadlocked, the test fails.
|
||||
//
|
||||
// Goroutines in the bubble use a synthetic time implementation.
|
||||
// The initial time is midnight UTC 2000-01-01.
|
||||
// Test must not be called from within a bubble.
|
||||
//
|
||||
// Time advances when every goroutine in the bubble is blocked.
|
||||
// For example, a call to time.Sleep will block until all other
|
||||
// goroutines are blocked and return after the bubble's clock has
|
||||
// advanced. See [Wait] for the specific definition of blocked.
|
||||
// The [*testing.T] provided to f has the following properties:
|
||||
//
|
||||
// Time stops advancing when f returns.
|
||||
//
|
||||
// If every goroutine is blocked and either
|
||||
// no timers are scheduled or f has returned,
|
||||
// Run panics.
|
||||
//
|
||||
// Channels, time.Timers, and time.Tickers created within the bubble
|
||||
// are associated with it. Operating on a bubbled channel, timer, or ticker
|
||||
// from outside the bubble panics.
|
||||
func Run(f func()) {
|
||||
synctest.Run(f)
|
||||
// - T.Cleanup functions run inside the bubble,
|
||||
// immediately before Test returns.
|
||||
// - T.Context returns a [context.Context] with a Done channel
|
||||
// associated with the bubble.
|
||||
// - T.Run, T.Parallel, and T.Deadline must not be called.
|
||||
func Test(t *testing.T, f func(*testing.T)) {
|
||||
synctest.Run(func() {
|
||||
testingSynctestTest(t, f)
|
||||
})
|
||||
}
|
||||
|
||||
//go:linkname testingSynctestTest testing/synctest.testingSynctestTest
|
||||
func testingSynctestTest(t *testing.T, f func(*testing.T))
|
||||
|
||||
// Wait blocks until every goroutine within the current bubble,
|
||||
// other than the current goroutine, is durably blocked.
|
||||
// It panics if called from a non-bubbled goroutine,
|
||||
// or if two goroutines in the same bubble call Wait at the same time.
|
||||
//
|
||||
// A goroutine is durably blocked if can only be unblocked by another
|
||||
// goroutine in its bubble. The following operations durably block
|
||||
// a goroutine:
|
||||
// - a send or receive on a channel from within the bubble
|
||||
// - a select statement where every case is a channel within the bubble
|
||||
// - sync.Cond.Wait
|
||||
// - time.Sleep
|
||||
//
|
||||
// A goroutine executing a system call or waiting for an external event
|
||||
// such as a network operation is not durably blocked.
|
||||
// For example, a goroutine blocked reading from an network connection
|
||||
// is not durably blocked even if no data is currently available on the
|
||||
// connection, because it may be unblocked by data written from outside
|
||||
// the bubble or may be in the process of receiving data from a kernel
|
||||
// network buffer.
|
||||
//
|
||||
// A goroutine is not durably blocked when blocked on a send or receive
|
||||
// on a channel that was not created within its bubble, because it may
|
||||
// be unblocked by a channel receive or send from outside its bubble.
|
||||
// Wait must not be called from outside a bubble.
|
||||
// Wait must not be called concurrently by multiple goroutines
|
||||
// in the same bubble.
|
||||
func Wait() {
|
||||
synctest.Wait()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright 2025 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 synctest_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"internal/testenv"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
)
|
||||
|
||||
// Tests for interactions between synctest bubbles and the testing package.
|
||||
// Other bubble behaviors are tested in internal/synctest.
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestFatal(t *testing.T) {
|
||||
runTest(t, func() {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
t.Fatal("fatal")
|
||||
})
|
||||
}, `^=== RUN TestFatal
|
||||
synctest_test.go:.* fatal
|
||||
--- FAIL: TestFatal.*
|
||||
FAIL
|
||||
$`)
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
runTest(t, func() {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
t.Error("error")
|
||||
})
|
||||
}, `^=== RUN TestError
|
||||
synctest_test.go:.* error
|
||||
--- FAIL: TestError.*
|
||||
FAIL
|
||||
$`)
|
||||
}
|
||||
|
||||
func TestSkip(t *testing.T) {
|
||||
runTest(t, func() {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
t.Skip("skip")
|
||||
})
|
||||
}, `^=== RUN TestSkip
|
||||
synctest_test.go:.* skip
|
||||
--- PASS: TestSkip.*
|
||||
PASS
|
||||
$`)
|
||||
}
|
||||
|
||||
func TestCleanup(t *testing.T) {
|
||||
done := false
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
ch := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
// This cleanup function should execute inside the test's bubble.
|
||||
// (If it doesn't the runtime will panic.)
|
||||
close(ch)
|
||||
})
|
||||
// synctest.Test will wait for this goroutine to exit before returning.
|
||||
// The cleanup function signals the goroutine to exit before the wait starts.
|
||||
go func() {
|
||||
<-ch
|
||||
done = true
|
||||
}()
|
||||
})
|
||||
if !done {
|
||||
t.Fatalf("background goroutine did not return")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
state := "not started"
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
go func() {
|
||||
state = "waiting on context"
|
||||
<-t.Context().Done()
|
||||
state = "done"
|
||||
}()
|
||||
// Wait blocks until the goroutine above is blocked on t.Context().Done().
|
||||
synctest.Wait()
|
||||
if got, want := state, "waiting on context"; got != want {
|
||||
t.Fatalf("state = %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
// t.Context() is canceled before the test completes,
|
||||
// and synctest.Test does not return until the goroutine has set its state to "done".
|
||||
if got, want := state, "done"; got != want {
|
||||
t.Fatalf("state = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeadline(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
defer wantPanic(t, "testing: t.Deadline called inside synctest bubble")
|
||||
_, _ = t.Deadline()
|
||||
})
|
||||
}
|
||||
|
||||
func TestParallel(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
defer wantPanic(t, "testing: t.Parallel called inside synctest bubble")
|
||||
t.Parallel()
|
||||
})
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
defer wantPanic(t, "testing: t.Run called inside synctest bubble")
|
||||
t.Run("subtest", func(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func wantPanic(t *testing.T, want string) {
|
||||
if e := recover(); e != nil {
|
||||
if got := fmt.Sprint(e); got != want {
|
||||
t.Errorf("got panic message %q, want %q", got, want)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("got no panic, want one")
|
||||
}
|
||||
}
|
||||
|
||||
func runTest(t *testing.T, f func(), pattern string) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
|
||||
f()
|
||||
return
|
||||
}
|
||||
t.Helper()
|
||||
re := regexp.MustCompile(pattern)
|
||||
testenv.MustHaveExec(t)
|
||||
cmd := testenv.Command(t, testenv.Executable(t), "-test.run=^"+t.Name()+"$", "-test.v", "-test.count=1")
|
||||
cmd = testenv.CleanCmdEnv(cmd)
|
||||
cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")
|
||||
out, _ := cmd.CombinedOutput()
|
||||
if !re.Match(out) {
|
||||
t.Errorf("got output:\n%s\nwant matching:\n%s", out, pattern)
|
||||
}
|
||||
}
|
||||
|
|
@ -421,6 +421,7 @@ import (
|
|||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
_ "unsafe" // for linkname
|
||||
)
|
||||
|
||||
var initRan bool
|
||||
|
|
@ -643,6 +644,7 @@ type common struct {
|
|||
cleanupPc []uintptr // The stack trace at the point where Cleanup was called.
|
||||
finished bool // Test function has completed.
|
||||
inFuzzFn bool // Whether the fuzz target, if this is one, is running.
|
||||
isSynctest bool
|
||||
|
||||
chatty *chattyPrinter // A copy of chattyPrinter, if the chatty flag is set.
|
||||
bench bool // Whether the current test is a benchmark.
|
||||
|
|
@ -1632,6 +1634,9 @@ func (t *T) Parallel() {
|
|||
if t.isParallel {
|
||||
panic("testing: t.Parallel called multiple times")
|
||||
}
|
||||
if t.isSynctest {
|
||||
panic("testing: t.Parallel called inside synctest bubble")
|
||||
}
|
||||
if t.denyParallel {
|
||||
panic(parallelConflict)
|
||||
}
|
||||
|
|
@ -1910,6 +1915,9 @@ func tRunner(t *T, fn func(t *T)) {
|
|||
// Run may be called simultaneously from multiple goroutines, but all such calls
|
||||
// must return before the outer test function for t returns.
|
||||
func (t *T) Run(name string, f func(t *T)) bool {
|
||||
if t.isSynctest {
|
||||
panic("testing: t.Run called inside synctest bubble")
|
||||
}
|
||||
if t.cleanupStarted.Load() {
|
||||
panic("testing: t.Run called during t.Cleanup")
|
||||
}
|
||||
|
|
@ -1975,11 +1983,55 @@ func (t *T) Run(name string, f func(t *T)) bool {
|
|||
return !t.failed
|
||||
}
|
||||
|
||||
// testingSynctestTest runs f within a synctest bubble.
|
||||
// It is called by synctest.Test, from within an already-created bubble.
|
||||
//
|
||||
//go:linkname testingSynctestTest testing/synctest.testingSynctestTest
|
||||
func testingSynctestTest(t *T, f func(*T)) {
|
||||
if t.cleanupStarted.Load() {
|
||||
panic("testing: synctest.Run called during t.Cleanup")
|
||||
}
|
||||
|
||||
var pc [maxStackLen]uintptr
|
||||
n := runtime.Callers(2, pc[:])
|
||||
|
||||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
t2 := &T{
|
||||
common: common{
|
||||
barrier: make(chan bool),
|
||||
signal: make(chan bool, 1),
|
||||
name: t.name,
|
||||
parent: &t.common,
|
||||
level: t.level + 1,
|
||||
creator: pc[:n],
|
||||
chatty: t.chatty,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancelCtx,
|
||||
isSynctest: true,
|
||||
},
|
||||
tstate: t.tstate,
|
||||
}
|
||||
t2.setOutputWriter()
|
||||
|
||||
go tRunner(t2, f)
|
||||
if !<-t2.signal {
|
||||
// At this point, it is likely that FailNow was called on one of the
|
||||
// parent tests by one of the subtests. Continue aborting up the chain.
|
||||
runtime.Goexit()
|
||||
}
|
||||
}
|
||||
|
||||
// Deadline reports the time at which the test binary will have
|
||||
// exceeded the timeout specified by the -timeout flag.
|
||||
//
|
||||
// The ok result is false if the -timeout flag indicates “no timeout” (0).
|
||||
func (t *T) Deadline() (deadline time.Time, ok bool) {
|
||||
if t.isSynctest {
|
||||
// There's no point in returning a real-clock deadline to
|
||||
// a test using a fake clock. We could return "no timeout",
|
||||
// but panicking makes it easier for users to catch the error.
|
||||
panic("testing: t.Deadline called inside synctest bubble")
|
||||
}
|
||||
deadline = t.tstate.deadline
|
||||
return deadline, !deadline.IsZero()
|
||||
}
|
||||
|
|
@ -2301,6 +2353,9 @@ func (t *T) report() {
|
|||
if t.parent == nil {
|
||||
return
|
||||
}
|
||||
if t.isSynctest {
|
||||
return // t.parent will handle reporting
|
||||
}
|
||||
dstr := fmtDuration(t.duration)
|
||||
format := "--- %s: %s (%s)\n"
|
||||
if t.Failed() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue