crypto news

Here’s how to test the software instructions in conjunction with the test/Synctest: Go 1.24

One of GO signature features is a compact support for synchronization. Goroutines and channels are primitive and effective to write simultaneous programs.

However, the simultaneous program test can be difficult and at risk of error.

At Go 1.24, we offer a new and experimental group testing/synctest A package to support the simultaneous symbol test. This publication will explain the motivation behind this experience, and explain how to use the Synctest package, and discuss its potential future.

In Go 1.24, testing/synctest The package is experimental and is not subject to the promise of consensus in Go. Not virtually visible. To use it, assemble your code with GOEXPERIMENT=synctest Set your environment.

It is difficult to test simultaneous programs

First of all, let’s think about a simple example.

the context.AfterFunc The job is arranged to summon a job in its Goroutine after the abolition of the context. Here is a possible test for 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.
}

We want to verify two conditions in this test: The job is not called before the context is canceled and the job He is He called after the abolition of the context.

It is difficult to check negative in a simultaneous system. We can easily test that the job has not been called yetBut how do we check this won’t Calling?

The common approach is waiting for some time before concluding that the event will not happen. Let’s try to provide an assistant job for 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 mm is not much time, but it adds to many tests.

This test is also: 10 millimeters is a long time on a fast computer, but it is not customary to see a temporary stop several seconds on the shared and loaded CI systems.

We can make the test less volatile at the expense of making it slower, and we can make it less slow at the expense of making it more effective, but we cannot make it fast and reliable.

Submit the test package/Synctest

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

The package contains only two functions: Run and Wait.

Run It calls for a new Goroutine job. This goroutine and i Goroutines started in an isolated environment that we call a bubble. Wait Each goroten is waiting for the current Goroutine bubble to ban it on another goroutine in the bubble.

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

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 identical to our original test, but we chose the test in A. synctest.Run Call and invite synctest.Wait Before confirming that the job was called or not.

the Wait A job awaits all goroutine in the caller bubble for its prohibition. Upon his return, we know that the context package may be called the job, or it will not be called it until we take some additional measures.

This test is now fast and reliable.

The test is also simpler: We have replaced us calledCh Channel with logical. Previously, we needed to use a channel to avoid data race between the Goroutine test AfterFunc Goroutine, however Wait The job now provides for synchronization.

Understand the race detector Wait Calls, and this test passes when running with -race. If we remove the second Wait Call, the race detector will correctly report the test race in the test.

Test time

The simultaneous code often deals with time.

The test code that works over time can be difficult. Using real time in tests leads to slow and brief tests, as we saw above. Using a false time requires avoiding time Package functions, code design is under testing with an optional fake watch.

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

Goroutines started in the bubble Run Use a fake watch. Inside the bubble, jobs in time The package works on a fake watch. Time is made in the bubble when all goroutines is banned.

To demonstrate, let’s write a test for context.WithTimeout job. WithTimeout A child creates a context, which ends after a certain time.

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 as if we were working with real time. The only difference is to turn the test function in synctest.RunAnd call synctest.Wait After each time.Sleep Call to wait until the time setting devices are over in the context package.

Ban and bubble

A major concept in testing/synctest Has it become bubble? It has been banned. This happens when every gorotin is banned in the bubble, and its ban can only be canceled by another gorotin in the bubble.

When a bubble is banned:

  • If there is a hanging Wait Call, come back.
  • Otherwise, the time is progressing the next time that Goroutine can separate, if any.
  • Otherwise, the bubble is clogged and Run Panic.

The bubble is not prohibited significantly if any gorotin is banned, but it may wake up by some events from outside the bubble.

The full menu that prevents Goroutine is:

  • Send or receive nothing
  • Send or receive on a channel created inside the same bubble
  • A specific phrase where each case prevents
  • time.Sleep
  • sync.Cond.Wait
  • sync.WaitGroup.Wait

Mutax

Operations on a sync.Mutex They do not prevent.

It is common for you to get global mutations. For example, a number of jobs are used in the reversal package of the global cache guarded by Mutex. If Goroutine is in the most large bubble blocks while getting goroten mutations out of the bubble, it is not categorically banned – it is prohibited, but it will be held by Goorotin from outside the bubble.

Since Mutexes is usually not held for long periods of time, we simply exclude them testing/synctestLook.

Channels

Channels created inside a bubble action differently from those created abroad.

Channel operations are categorically prevented if the channel is controlled (created in the bubble). It works on a bubble channel from outside the bubble.

These rules guarantee that Goroutine was dramatically banned when communicating with Goroutines inside its bubble.

Me/h

Input/external output operations, such as reading from the network connection, do not prevent.

Network readings may be not prohibited by writing from outside the bubble, and perhaps even from other operations. Even if the only writer who communicates with the network is also in the same bubble, the operating time cannot distinguish between a connection pending the arrival of more data and one as Kernel has received data in the process of delivering it.

A network server or customer server with Synctesst will generally provide a fake network application. For example, and net.Pipe The job creates a pair of net.ConnS that uses a network connection in memory and can be used in Synctesst tests.

Age bubble

the Run Goroutine function begins in a new bubble. He returns when all Goorotin came out in the bubble. From panic if the bubble is prohibited significantly and its ban can not be canceled due to the progress of time.

The requirements that each goroten in the bubble outlet before the return is to run means that the tests should be keen to clean any gorostines background before completion.

Network code test

Let’s take a look at another example, this time using testing/synctest Package to test a network connected to the network. For this example, we will test net/http Address the package of 100 response follow -up.

A HTTP customer who sends a header “expect: 100 eradication” to tell the server that the customer has additional data to send. The server then may respond using 100 media response to request the rest of the request, or with some other case to inform the customer that the content is not required. For example, the customer may use a large file to download this feature to confirm that the server is ready to accept the file before sending it.

Our test will confirm that when sending a header: 100-hontinue “, a HTTP customer does not send the request content before the server requests it, and that he sends the content after receiving 100 follow-up response.

Tests of a customer and communication server can often use a retrieval network connection. When working with testing/synctestHowever, we will usually want to use a fake network connection to allow us to discover when all goroutines is blocked on the network. We will start this test by creating a file http.Transport (HTTP customer) uses a network connection in the memory that was created by 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 transfer in the head of the “Expect: 100 drain”. The request is sent in a new Goroutine, because it will not be completed until the end of the test.

        body := "request body"
        go func() {
            req, _ := http.NewRequest("PUT", " 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 heads sent by the customer.

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

Now we have reached the heart of the test. We want to confirm that the customer will not send the application body yet.

We start a new goroutine copy the body sent to the server to the server strings.BuilderWait all goroutines in the bubble to block it, and check that we have not yet read anything from the body.

If we forget synctest.Wait The call, the race detector will correctly complain about the data race, but with 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 Follow -up” response to the customer and we are investigating that he is now sending the application 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)
        }

Finally, we have finished sending a “200 OK” response to conclude the request.

We started many goroutines during this test. the synctest.Run You will wait for the call until they all 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 checking the requesting body if the servant does not ask for it, or it is sent if the servant does not respond within a deadline.

Experience

We offer testing/synctest In Go 1.24 k experimental eviction. Depending on the comments and experience, we may release them with or without modifications, follow the experiment, or remove it in a future version of Go.

The package is virtually invisible. To use it, assemble your code with GOEXPERIMENT=synctest Set your environment.

We want to hear your notes! If you try testing/synctestPlease report your experiences, positive or negative, Go.dev/issue/67434.


Credits: Damian Neil

Photo by Gabriel Josao on non -Zubash

This article is available on Go Blog Under CC with a verb license 4.0.

Related Articles

Leave a Reply

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

Back to top button

Adblock Detected

Please consider supporting us by disabling your ad blocker