Skip to content

testscript/plugin: new package - #307

Merged
rogpeppe merged 1 commit into
masterfrom
048-plugins
Jul 1, 2026
Merged

rogpeppe merged 1 commit into
masterfrom
048-plugins

Conversation

@rogpeppe

Copy link
Copy Markdown
Owner

This adds the capability for a testscript script to use a "plugin", an external binary that implements support for additional functionality not easily implemented with a regular command. Except when using the testscript command, it must be explicitly enabled by using plugin.Setup.

A plugin binary is run as an RPC server communicating over stdin/stdout, and can provide extra commands and environment variables to a testscript test. A classic use case is to run a scratch server, set an environment variable referring to its address, and tear it down when a test is done.

For now we use a custom RPC implementation so we can avoid polluting the Go API (unfortunately net/rpc requires that the types used are made public).

When plugin is invoked inside a testscript, e.g.

plugin foo

a plugin binary is searched for with the name testscript-plugin-foo.

For reference, we implement the existing Go proxy support as a plugin. In the future we may decide to fully factor out this support from the core testscript executable, although we'll have to be careful about backward compatibility concerns.

Comment thread testscript/testscript.go Outdated
if v := os.Getenv(name); v != "" {
env.Vars = append(env.Vars, name+"="+v)
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we removed SYSTEMROOT in 3030644, why are you re-adding these? note how os/exec always passes this one along now.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread testscript/plugin/contract.go Outdated
// NeedStderr and NeedStdout indicate whether the command produces
// standard error or standard output respectively.
NeedStderr bool
NeedStdout bool

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these never seem to be read by the non-test logic? either wire them properly or remove

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wired up and changed name to be more appropriate.


// call sends a request and waits for its response, unmarshaling the result
// into result (if non-nil). A non-empty response error is returned as an error.
func (cl *client) call(method string, arg, result any) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my instinct is that this should take a context to be able to use timeouts/deadlines or cancel, as otherwise a plugin over RPC can hang or take a long time to reply, hanging the caller too.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it can block indefinitely but currently there's no notion of context or timeout in command interface, so I think it's OK to just let it block for now, as with any other command implementation. We could add timeouts as an enhancement later.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a TODO

Comment thread testscript/plugin/plugin.go Outdated
if err != nil {
return nil, err
}
cmd.Stderr = os.Stderr

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that if you send stderr straight to os.Stderr, any failures happening during go test will be mixed rather than being matched to the correct test run via e.g. testing.T.Log. perhaps not a big deal.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread testscript/plugin/contract.go Outdated

// CWD is the absolute path of the current working directory the
// command should run in.
CWD string

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd personally name this Dir, for consistency with os/exec and most other Go APIs

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread testscript/plugin/contract.go Outdated
// CmdParams holds the parameters for running a plugin command.
type CmdParams struct {
// CmdName is the name of the command being run.
CmdName string

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just Name? the other fields are not named CmdArgs etc.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread testscript/plugin/contract.go Outdated

// Environ holds the values of the variables named in the command's
// [CmdInfo.RequiredEnv], as resolved in the test environment.
Environ map[string]string

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the name Environ in Go makes me think of os.Environ, which is a []string, not a map. so if you really do want to use a map, I'd use a different name like Env

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


// Stdin holds the standard input for the command.
Stdin []byte
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like this plugin system doesn't support background commands. that's probably fine for the RPC model, but do we test that these are rejected properly?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure there's much need: & is only supported for exec commands not commands in general.

}

// PluginInfo holds static information about a plugin.
type PluginInfo struct {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally I'd add something like a Version int here, starting at 1, such that we can later bump this if we need to do any significant changes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, a bit differently, allowing for version negotiation.

@rogpeppe
rogpeppe force-pushed the 048-plugins branch 2 times, most recently from 2c7a5ac to 7f763ef Compare July 1, 2026 13:34
This adds the capability for a testscript script to use a "plugin", an
external binary that implements support for additional functionality not
easily implemented with a regular command. Except when using the
testscript command, it must be explicitly enabled by using
`plugin.Setup`.

A plugin binary is run as an RPC server communicating over stdin/stdout,
and can provide extra commands and environment variables to a testscript
test. A classic use case is to run a scratch server, set an environment
variable referring to its address, and tear it down when a test is done.

For now we use a custom RPC implementation so we can avoid polluting the
Go API (unfortunately `net/rpc` requires that the types used are made
public).

When `plugin` is invoked inside a testscript, e.g.

```
plugin foo
```

a plugin binary is searched for with the name `testscript-plugin-foo`.
@rogpeppe
rogpeppe merged commit 49d4e9d into master Jul 1, 2026
6 checks passed
@rogpeppe
rogpeppe deleted the 048-plugins branch August 4, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants