Documentation
¶
Index ¶
- Constants
- Variables
- func EventuallyCmd(cmds map[string]func(*testscript.TestScript, bool, []string)) func(*testscript.TestScript, bool, []string)
- func GnokeyTSCmd() func(ts *testscript.TestScript, neg bool, args []string)
- func GpaoTSCmd(cfg GpaoConfig) func(*testscript.TestScript, bool, []string)
- func HTTPGetCmd() func(ts *testscript.TestScript, neg bool, args []string)
- func RepeatCmd(cmds map[string]func(*testscript.TestScript, bool, []string)) func(ts *testscript.TestScript, neg bool, args []string)
- func ResolveScriptFiles(args []string) ([]string, error)
- func Run(cfg RunConfig) error
- func RunT(t testscript.T, cfg RunConfig)
- func SleepCmd() func(ts *testscript.TestScript, neg bool, args []string)
- func TSValidateError(ts *testscript.TestScript, cmd string, neg bool, err error)
- func ValidatorTSCmd(cl *cluster.Cluster) func(ts *testscript.TestScript, neg bool, args []string)
- type ClusterOverrides
- type ClusterSpec
- type GpaoConfig
- type RunConfig
- type Scenario
- type TestscriptT
Constants ¶
const ( NodeConfigPrefix = "config." GenesisParamsPrefix = "genesis." )
The two generic key families, one per typed target they are applied to.
const HarnessAssignedReason = "the harness assigns each node its listen ports and its peers"
Variables ¶
var HarnessAssignedConfigKeys = []string{"rpc.laddr", "p2p.laddr", "p2p.persistent_peers"}
HarnessAssignedConfigKeys are the node config paths a scenario cannot set, and HarnessAssignedReason says why.
The peer list is here for the same reason as the listen addresses, and it hides better: the harness writes each validator a list of its own, and a scenario's overrides are applied afterwards, once, identically to every node. A cluster whose validators were all handed the same peers reaches no quorum and commits no block.
var NamedClusterKeys = []string{"validators", "code-submission-policy", "pkg-approver", "block-max-gas"}
NamedClusterKeys are the keys with a name of their own, as opposed to the two prefixed families. The switch in parseClusterSection is what accepts them; this is what the listing reads, so the two cannot drift apart without the test that compares them going red.
Functions ¶
func EventuallyCmd ¶
func EventuallyCmd(cmds map[string]func(*testscript.TestScript, bool, []string)) func(*testscript.TestScript, bool, []string)
EventuallyCmd returns a testscript command that reruns a subcommand until it succeeds or the timeout passes.
timeout bounds when a new attempt starts, not the wall-clock time of the command itself: the deadline is only checked between attempts, so a slow or hanging sub-command can overrun it. A hard per-attempt cutoff would need to abandon a still-running attempt from another goroutine, leaving ts in a half-mutated state, which is worse than overrunning. The outer test run timeout is the real backstop for a hung command.
It dispatches only to the custom command map, not to testscript builtins, because the builtin table is not reachable from a user command.
There is no chain event announcing that a package went live, so every assertion about the oracle is a poll. This is what keeps those polls from being fixed sleeps.
Every attempt starts with the builtin output buffers emptied, so a following "stdout", "! stdout", "stderr", "! stderr" or "cp stdout" sees the attempt that succeeded and nothing else. That is what lets a read needing a negation or a capture still be retried instead of running once and flaking on a transient RPC blip.
It does not poll a following assertion: without a gate it returns as soon as the sub-command exits 0, so a "stdout <pattern>" line after it runs exactly once, against whatever that first success produced. That is unsound for a query answering an empty result rather than an error -- vm/qinertpaths lists nothing and exits 0 -- because the wait ends before the value arrives.
"-stdout <regex>" is the answer to that: the pattern is checked inside the attempt, so an exit 0 whose output does not match is not yet an answer and the wait runs the command again. HTTPGetCmd's second argument gates its own body the same way (httpget.go:86).
func GnokeyTSCmd ¶
func GnokeyTSCmd() func(ts *testscript.TestScript, neg bool, args []string)
GnokeyTSCmd returns a testscript command handler for gnokey. It auto-injects -home, -insecure-password-stdin and -remote.
func GpaoTSCmd ¶
func GpaoTSCmd(cfg GpaoConfig) func(*testscript.TestScript, bool, []string)
GpaoTSCmd returns the "gpao" testscript command, which accepts start, stop and restart.
The oracle's lifetime is owned here rather than in the script so that a script that fails part way still leaves no daemon behind, and so that readiness is a probe rather than a sleep.
func HTTPGetCmd ¶
func HTTPGetCmd() func(ts *testscript.TestScript, neg bool, args []string)
HTTPGetCmd returns a testscript command that fetches a URL and writes the response body where the stdout matcher can see it.
This is how a scenario reads the oracle's own verdict. The status board is the only machine-readable place the oracle distinguishes rejected, pending, gave_up and blocked; everywhere else those live in operator output.
An optional second argument gates on the body: the status board answers HTTP 200 for every verdict it knows, including "unknown", so without a pattern "eventually ... http_get" would return on the first response no matter what it says. The gate has to live in here rather than as a following "stdout <pattern>": that check runs only once eventually has already returned, so it would report whatever verdict the poll stopped on instead of waiting for the one the scenario is after.
func RepeatCmd ¶
func RepeatCmd(cmds map[string]func(*testscript.TestScript, bool, []string)) func(ts *testscript.TestScript, neg bool, args []string)
RepeatCmd returns a testscript command that runs a subcommand N times. It takes a reference to the full commands map for dispatching.
func ResolveScriptFiles ¶
ResolveScriptFiles turns command arguments into the scripts to run. Accepts no arguments (the tour), directories, .txtar files, or a mix.
A directory contributes its own scripts in filename order and never its subdirectories, and a named file contributes itself, so the caller's order survives as the run order.
func RunT ¶
func RunT(t testscript.T, cfg RunConfig)
RunT drives one script through testscript.T, which a real *testing.T satisfies: a go test driver can hand its own T here and get the script's transcript in the test output rather than through a logger of our own.
func SleepCmd ¶
func SleepCmd() func(ts *testscript.TestScript, neg bool, args []string)
func TSValidateError ¶
func TSValidateError(ts *testscript.TestScript, cmd string, neg bool, err error)
TSValidateError checks a command result against the negation flag. If err != nil and neg is false, it fatals. If err == nil and neg is true, it fatals.
func ValidatorTSCmd ¶
func ValidatorTSCmd(cl *cluster.Cluster) func(ts *testscript.TestScript, neg bool, args []string)
ValidatorTSCmd returns a testscript command that stops and restarts individual validators, so a scenario can inject a node outage.
Lifecycle is driven from Go rather than through txtar's exec primitive: a script says which node should be down and for how long, and the harness owns signalling it, waiting for it to be gone, and bringing it back ready. A script that spawned processes itself would also have to clean them up, and would leak one on every failed assertion.
Indexing matches the RPC_ADDR_N the scripts already use.
Only restart is negatable: "! validator restart N" asserts that the node cannot come back, and the error, which carries the node's stderr tail, reaches the script's stderr so the scenario can name the reason it died. It asserts that narrowly: a node the cluster does not have, or one that was never stopped, fails the script in either mode. A stop that fails is a harness fault, not something a scenario observes.
Types ¶
type ClusterOverrides ¶
type ClusterOverrides struct {
Validators *int
CodeSubmissionPolicy *string
// PkgApprover is a bech32 address here, where a script names a role.
// ClusterSpec.ApplyTo accepts either.
PkgApprover *string
BlockMaxGas *int64
}
ClusterOverrides are the cluster settings named on the command line. Each is nil unless it was actually given, which is what separates a flag left at its default from one set to that same value.
A named setting wins over what a script declares, and the caller owns the result: running a scenario that declared three validators against two is allowed, and that scenario may go red. That is the point of an override.
func (ClusterOverrides) Apply ¶
func (o ClusterOverrides) Apply(spec ClusterSpec) ClusterSpec
Apply returns the cluster a script will actually run against, leaving the declaration itself untouched. A setting named on the command line wins over the one the script declared.
type ClusterSpec ¶
type ClusterSpec struct {
// Validators is the only setting with no usable zero value, so it is the
// only one a scenario must state.
Validators int
// CodeSubmissionPolicy is empty for the chain default.
CodeSubmissionPolicy string
// PkgApprover names who may enable inert packages, as a role rather than
// an address, because the addresses are derived from mnemonics the runner
// owns. Empty means the oracle, which is provisioned for every run.
// "user" means the test user, which is what leaves the oracle
// unauthorized.
PkgApprover string
// BlockMaxGas is zero for the chain default.
BlockMaxGas int64
// NodeConfig holds the "config." keys, prefix stripped, in declaration
// order. Each is a `gnoland config set` key set on every node.
NodeConfig []cluster.Override
// GenesisParams holds the "genesis." keys, prefix stripped, in declaration
// order. Each is a `gnogenesis params set` key set on the genesis params.
GenesisParams []cluster.Override
}
ClusterSpec is the cluster one scenario declares it needs.
It is the projection of cluster.ClusterConfig a scenario is allowed to choose, not a copy of it: the settings a script may state, in the vocabulary a script states them in, before the runner resolves them against the keys and binaries it owns.
func ParseClusterSpec ¶
func ParseClusterSpec(script []byte) (ClusterSpec, error)
ParseClusterSpec reads the cluster a script declares. Unknown keys are errors rather than ignored, so a typo fails at parse time instead of silently booting the wrong chain.
func (ClusterSpec) ApplyTo ¶
func (s ClusterSpec) ApplyTo(cfg *cluster.ClusterConfig, user, oracle crypto.Address) error
ApplyTo settles the spec onto a cluster config, leaving every option the scenario did not state at the chain default.
The approver is resolved here rather than in the section, because a script names a role while genesis needs an address, and the addresses come from mnemonics the runner owns.
type GpaoConfig ¶
type GpaoConfig struct {
// Binary yields the oracle binary, building it if the run has not needed
// one yet. A provider rather than a path, because a run whose scripts
// never start the oracle must not pay to build it.
Binary func() (string, error)
Mnemonic string
ChainID string
Remote string // -remote used when the script names no node
GnoRoot string
}
GpaoConfig carries what stays fixed for a whole run. Per-invocation flags come from the script.
type RunConfig ¶
type RunConfig struct {
// ScriptPath is the single script this run drives. One script per run,
// because the cluster it names in RPCAddr was booted for it alone.
ScriptPath string
RPCAddr string
// RPCAddrs holds every validator's RPC address in index order. Scripts see
// them as RPC_ADDR_0 upward. RPCAddr stays the first validator.
RPCAddrs []string
ChainID string
GnoHome string
UserAddr string
KeyName string
Verbose bool
Logger *slog.Logger
Gpao GpaoConfig
// GpaoKeyName is the keybase name under which the oracle's key was
// imported, exported to scripts as GPAO_KEY_NAME.
GpaoKeyName string
// Cluster is the local cluster the scripts run against, so a scenario can
// stop and restart individual validators. Nil only when no local cluster
// was started for the script.
Cluster *cluster.Cluster
// GpaoAddr is the oracle's approver address, exported to scripts as
// GPAO_ADDR. GpaoKeyName names the keybase entry; a scenario querying the
// approver's own balance needs the address itself.
GpaoAddr string
}
RunConfig holds resolved values for running one txtar script.
type Scenario ¶
type Scenario struct {
Spec ClusterSpec
Path string
}
Scenario is one script and the cluster it runs against.
func ResolveScenarios ¶
func ResolveScenarios(paths []string, overrides ClusterOverrides) ([]Scenario, error)
ResolveScenarios reads each script's declared cluster and settles the command line's overrides over it.
One scenario per script, so every one of them gets a chain built from genesis and torn down after. A chain carries deployed packages, account balances and its height, and none of that is reset while it runs, so two scenarios sharing one would make the second's result depend on the first: on what it deployed, on what it spent, and on the name that decided which ran first. That is a suite where adding a file changes the verdict of a file nobody touched.
Scenarios come back in the caller's order.
type TestscriptT ¶
type TestscriptT struct {
Logger *slog.Logger
Failed bool
Skipped bool
// contains filtered or unexported fields
}
TestscriptT adapts slog.Logger to satisfy testscript.T. It uses runtime.Goexit() for FailNow/Fatal/Skip to match the semantics that testscript expects (same as testing.T).
func NewTestscriptT ¶
func NewTestscriptT(logger *slog.Logger, verbose bool) *TestscriptT
NewTestscriptT creates a TestscriptT with the given logger and verbosity.
func (*TestscriptT) FailNow ¶
func (t *TestscriptT) FailNow()
func (*TestscriptT) Fatal ¶
func (t *TestscriptT) Fatal(args ...any)
func (*TestscriptT) Log ¶
func (t *TestscriptT) Log(args ...any)
func (*TestscriptT) Parallel ¶
func (t *TestscriptT) Parallel()
func (*TestscriptT) Run ¶
func (t *TestscriptT) Run(name string, f func(testscript.T))
Run executes f in an isolated goroutine so that runtime.Goexit() within f only terminates that goroutine, not the caller. This matches how testing.T.Run works internally.
func (*TestscriptT) Skip ¶
func (t *TestscriptT) Skip(args ...any)
func (*TestscriptT) Verbose ¶
func (t *TestscriptT) Verbose() bool