Skip to content

proposal: testing: new API to simplify calling a function in a child process #79941

Description

@adonovan

Background: the testing package makes it easy to check that specific inputs to a function produce expected outputs. When a function reports failure by returning an error, failure cases are easy to test in the same way. For functions that fail by panicking, it is sometimes possible for the test to use recover, but as complexity grows this approach quickly becomes fragile. For a function that fails by calling os.Exit or log.Fatal or by panicking in a background goroutine or running out of memory, it is impossible to write a reliable test in a single process. Similarly, when testing programs (or whole applications) that access global resources, such as stdout or signal handlers, a single process will not do.

In such cases, it is common to create a new process for the function under test, using os/exec. But it is quite common for all of the code of the child process to already be linked into the test executable, so it is complex and wasteful for the test to invoke go build to create the child executable. A common workaround is for the test to invoke its own executable as a child process, but in a different mode in which instead of running testing.MainStart, it runs the main function of the application under test. The mode is typically indicated by an environment variable.

Proposal: We propose to add new API to the testing package to make this kind of test easier to write. Here's an example of a test that creates a subprocess to call a function that prints "hello, world" then terminates the process with an error:

package my_test

import (
    "fmt"
    "os"
    "testing"
)

var hello = testing.NewEntrypoint(func() {
    fmt.Println("hello, world")
    os.Exit(42)
}
    
func Test(t *testing.T) {
   // Call hello, world function in a subprocess.
   cmd := hello.Command(t)
   cmd.Stdout = t.Output()
   cmd.Stderr = t.Output()
   cmd.Run() // prints "hello, world" and returns ExitError(42)
}    

The new API is:

package testing

// An Entrypoint represents a function within the test executable to be used as
// alternative entrypoint (to MainStart) when running the test executable as a
// child process.
type Entrypoint struct{ ... }

// Command returns an exec.Cmd that, when run, will execute the entrypoint
// function in a child process.
//
// The caller must not remove the first item from the returned command's Env field,
// since it is needed by entrypoint machinery.
func (e *Entrypoint) Command(t TB) *exec.Cmd

// NewEntrypoint registers fn, an alternative entrypoint to MainStart, so that
// it may be called in a process of its own. The returned Entrypoint can be
// used to create a new child process.
//
// NewEntrypoint must be called during package initialization, so that it is
// registered in both the parent and the child processes. It is typically
// assigned to a package-level variable. For example: [... see above ... ]
//
// NewEntrypoint must not be called more than once for a given function.
func NewEntrypoint(fn func()) *Entrypoint

An implementation can be found in https://go.dev/cl/789340.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions