Uk News

How to test the simultaneous code with test/synctest is described below: GO 1.24

One of the signature features of GO is settled for simultaneity. Goroutins and channels are simple and effective primitives for writing simultaneous programs.

However, simultaneous programs may be difficult to test and prone to error.

A new, experimental at GO 1.24 testing/synctest Package simultaneous code to support the test. This article will explain the motivation behind this experiment, show how to use the synctest package and discuss its potential future.

At GO 1.24 testing/synctest The package is experimental and is not subject to GO compliance promise. It does not appear by default. Component with your code to use GOEXPERIMENT=synctest Adjust it in your environment.

Hard to test simultaneous programs

Imagine a simple example to start.

. context.AfterFunc After a context is canceled, it regulates the call of a function its own goroutine. Here is a possible test AfterFunc:

func TestAfterFunc(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())

    calledCh := make(chan struct{}) // closed when AfterFunc is called
    context.AfterFunc(ctx, func() {
        close(calledCh)
    })

    // TODO: Assert that the AfterFunc has not been called.

    cancel()

    // TODO: Assert that the AfterFunc has been called.
}

In this test, we want to check two conditions: the function is not called before the context is canceled and the function that The context is called after cancellation.

It is difficult to control the negative in a simultaneous system. We can easily test that the function is not called yetBut how do we check it will not be is it to be called?

A widespread approach is to wait a little before it concludes that an event will not take place. Let’s try to bring an assistant function to our test.

// funcCalled reports whether the function was called.
funcCalled := func() bool {
    select {
    case <-calledCh:
        return true
    case <-time.After(10 * time.Millisecond):
        return false
    }
}

if funcCalled() {
    t.Fatalf("AfterFunc function called before context is canceled")
}

cancel()

if !funcCalled() {
    t.Fatalf("AfterFunc function not called after context is canceled")
}

This test is slow: 10 milliseconds are not much time, but it adds many tests.

This test is also a long time on a fast computer for 10 milliseconds, but it is not unusual to see pauses that last for a few seconds in shared and overloaded CI systems.

At the expense of making the test slower, we can make less porridge and make it less slow at the expense of making pants, but we cannot make it both fast and reliable.

Introduction of Test/Synctest Package

. testing/synctest The package solves this problem. This test allows us to rewrite in a simple, fast and reliable without any changes in the tested code.

The package contains only two functions: Run And Wait.

Run Calls a new goroutine function. This goroutine and any goroutin initiated with it, bubble. Wait Every goroutine in the balloon of the current Goroutine expects to block another Goroutin in the balloon.

Let’s rewrite using our test above. testing/synctest package.

func TestAfterFunc(t *testing.T) {
    synctest.Run(func() {
        ctx, cancel := context.WithCancel(context.Background())

        funcCalled := false
        context.AfterFunc(ctx, func() {
            funcCalled = true
        })

        synctest.Wait()
        if funcCalled {
            t.Fatalf("AfterFunc function called before context is canceled")
        }

        cancel()

        synctest.Wait()
        if !funcCalled {
            t.Fatalf("AfterFunc function not called after context is canceled")
        }
    })
}

This is almost the same as our original test, but the test is synctest.Run Call and we call synctest.Wait Before claiming that the function was called or not called.

. Wait The function waits for each goroutin to block the caller’s balloon. When you come back, we know that the context package is looking for a function or it will not call it until you do more.

This test is now both fast and reliable.

The test is also simpler: calledCh Channel with boolean. Previously, we had to use a channel to avoid a data race with Test Goroutine. AfterFunc Goroutine, but Wait The function now provides this synchronization.

Race Detector Understands Wait It passes when it calls and works with this test -race. Secondly, if we remove Wait Call, the race detector will correctly report a data race in the test.

Test time

Simultaneous code usually takes care of time.

The test code that runs over time can be difficult. Using real -time in the tests causes slow and lap lap tests as we have seen above. Using fake time requires avoidance time Package functions and design of the code tested to work with an optional fake clock.

. testing/synctest The package makes it easier to test the code that uses time.

Started by bubble goroutins Run Use a fake watch. In the bubble, functions time The package works at a fake clock. When all goroutins are blocked, time progresses in the balloon.

To show, let’s write a test context.WithTimeout function. WithTimeout It forms the child of a context that ends after a certain time -out.

func TestWithTimeout(t *testing.T) {
    synctest.Run(func() {
        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()
        if err := ctx.Err(); err != nil {
            t.Fatalf("before timeout, ctx.Err() = %v; want nil", 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", err)
        }
    })
}

We write this test just like we work in real time. The only difference is that we can surround the test function. synctest.Runand call synctest.Wait after each time.Sleep Call to wait for the timers of the context package to finish the work.

Blocking and Balloon

A key concept testing/synctest Is it a bubble Durable Invited. This happens when every goroutin in the balloon is blocked and only by another Goroutin in the balloon.

When a bubble is prevented in a durable way:

  • An extraordinary Wait Call, returns.
  • Otherwise, the next time, the next time will prevent a goroutin.
  • Otherwise, the bubble will be locked and Run panic.

If any goroutin is blocked, but can be awakened by an event outside the bubble, a balloon is not prevented in a durable way.

Full list of operations that block a goroutini durable way:

  • A reference or receiving on the Nile channel
  • A reference or reception blocked on a channel created in the same balloon
  • A statement of selection in which each case prevents durability
  • time.Sleep
  • sync.Cond.Wait
  • sync.WaitGroup.Wait

Mutual

Operations sync.Mutex not resistant.

It is common for functions to obtain a global mutual. For example, a number of functions in the reflection package use a global cache protected by a muteks. If the synctest bubble blocks when obtaining a museum held by a goroutin outside the bubble, it is not unbearable – prevented, but it is blocked by a Goroutin from outside the balloon.

Since the mutuals are usually not kept for a long time, we simply exclude them testing/synctestthought.

Channels

The channels formed in a balloon behave differently from the outside created.

If the channel operations only foam the channel (formed in the balloon), it is prevented by resistance. It works on a canal foaming from outside the bubble panic.

These rules ensure that a goroutinin is only durable when communicating with goroutins in bubbles.

G/ç

External G/O procedures such as reading from a network connection are not unbearable.

Network readings can probably be prevented from other operations by writing from outside the balloon. Even if the only author of a network connection is in the same balloon, the working time cannot distinguish between a connection waiting for more data and that the nucleus receives data and is in the process of delivery.

Testing a network server or client with synctest usually requires a fake network application. For example, net.Pipe Function creates a pair net.ConnS.

Bubble Lifetime

. Run The function starts a goroutin on a new bubble. Every goroutin in the bubble comes back when it comes out. Panic if the bubble is blocked in a durable way and cannot be prevented by the progress time.

It means that each goroutin at the bubble output should pay attention to cleaning any background goroutin before the tests are completed before the work refunds.

TEST OF THE NETWORK Code

Let’s look at another example, this time testing/synctest Package to test a program connected to the network. We will test for this example net/http The package of the 100 ongoing response.

A HTTP client who sends a request may include a “Expectation: 100-Bart” title to tell the server that the client has additional data to be sent. The server may then respond to another situation to request the rest of the request, or to another situation to tell the customer to tell the customer that the content is not necessary. For example, a client uploading a large file may use this feature to confirm that the server is willing to accept the file before sending.

Our test will verify that the HTTP client does not send the content of a request before requesting a server when sending a “Expectation: 100-Tecren” title, and that it sends the content after receiving 100 continuation response.

Usually a client and the server’s tests can use a backward network connection. While working with testing/synctestHowever, we will usually want to use a fake network connection to allow us to determine when all goroutins are blocked on the network. This test is a http.Transport (A HTTP client) created by a network connection in memory net.Pipe.

func Test(t *testing.T) {
    synctest.Run(func() {
        srvConn, cliConn := net.Pipe()
        defer srvConn.Close()
        defer cliConn.Close()
        tr := &http.Transport{
            DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
                return cliConn, nil
            },
            // Setting a non-zero timeout enables "Expect: 100-continue" handling.
            // Since the following test does not sleep,
            // we will never encounter this timeout,
            // even if the test takes a long time to run on a slow machine.
            ExpectContinueTimeout: 5 * time.Second,
        }

We send a request to this transport with the title set “Expectation: 100-Tahmin”. Since the test will not be completed until the end of the test, a new goroutine is sent.

        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", err)
            } else {
                resp.Body.Close()
            }
        }()

We read the request titles sent by the customer.

        req, err := http.ReadRequest(bufio.NewReader(srvConn))
        if err != nil {
            t.Fatalf("ReadRequest: %v", err)
        }

Now we come to the heart of the test. We want to claim that the customer will not send the request body yet.

We start a new goroutine that copies the body sent to the server. strings.BuilderWait all the goroutins in the balloon to be blocked and verify that we have not yet read anything from the body.

If we forget synctest.Wait Call, the race detector will complain correctly from a data race, but Wait This is safe.

        var gotBody strings.Builder
        go io.Copy(&gotBody, req.Body)
        synctest.Wait()
        if got := gotBody.String(); got != "" {
            t.Fatalf("before sending 100 Continue, unexpectedly read body: %q", got)
        }

We write the “100 continue” response to the customer and now we confirm that it sends the request body.

        srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n"))
        synctest.Wait()
        if got := gotBody.String(); got != body {
            t.Fatalf("after sending 100 Continue, read body %q, want %q", got, body)
        }

And finally, we finish by sending the “200 arrow” response to conclude the request.

During this test we started a few goroutin. . synctest.Run The call will wait for all of them to come out before returning.

        srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
    })
}

This test can be easily expanded to test other behaviors such as verifying that the request body is not sent if the server does not want, or if the server does not respond within a time -out.

Try the Status

We introduce testing/synctest At GO 1.24 experimental package. Depending on feedback and experience, we can release, continue to try, or remove it in a future version of GO.

The package does not appear by default. Component with your code to use GOEXPERIMENT=synctest Adjust it in your environment.

We want to hear your feedback! If you try testing/synctestPlease report your experience positively or negatively. go.dev/issue/67434.


Loans: Damien Neil

Photo: Gabriel Gusmao

This article is available at this address Go blog 4.0 Land Registry License under CC.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button