cli

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 38 Imported by: 0

Documentation

Overview

Package cli contains side-effect-free command orchestration for FlowBaton.

Index

Constants

View Source
const (
	ExitOK      = 0
	ExitInvalid = 2

	CheckSyntaxUsage = "usage: flowbaton check-syntax FILE|-\n"
)
View Source
const ExitFailure = 1

ExitFailure is the exit code for a run that was understood and did not pass.

View Source
const TestUsage = "usage: flowbaton test [options] FILE|DIR...\n"

TestUsage is the one-line usage for the subcommand.

Variables

View Source
var TopLevelSubcommands = []string{
	"check-syntax",
	"test",
	"list-devices",
	"start-device",
	"hierarchy",
	"query",
	"bugreport",
	"driver-setup",
	"mcp",
}

TopLevelSubcommands is the canonical list of subcommands flowbaton dispatches. generate-completion reads it so a new command shows up in completion the moment it is added here; a test pins the set so dispatch and completion do not silently drift apart.

Functions

func DefaultOutputDirectory

func DefaultOutputDirectory(options TestOptions, home string, now time.Time) string

DefaultOutputDirectory resolves where a run's artifacts go.

specs/03-cli-tooling.md section 1 gives the precedence: an explicit --test-output-dir wins, then --debug-output, then a timestamped directory under the user's home. --flatten-debug-output drops the timestamp segment, which is what a CI job wants when it collects a fixed path.

func ExitCodeFor

func ExitCodeFor(err error) int

ExitCodeFor classifies an error into the documented exit codes. Anything not explicitly a usage error is a failure: a misclassification that returned OK would report a green suite that never ran.

Types

type ArtifactSink

type ArtifactSink struct {
	// contains filtered or unexported fields
}

ArtifactSink writes run artifacts, into one of two directories.

A name the flow author wrote resolves against the process working directory: a flow saying `takeScreenshot: settings` leaves ./settings.png where the operator ran from, whichever directory the flow itself lives in. Everything the author did not name — the automatic failure captures — is the run's own bookkeeping and stays in the run directory.

func NewArtifactSink

func NewArtifactSink(directory, authoredDirectory string) *ArtifactSink

func (*ArtifactSink) Write

type BugreportRunner

type BugreportRunner struct {
	Collect       func(ctx context.Context, serial, outputPath string) error
	CollectIOS    func(ctx context.Context, udid, outputPath string) error
	ResolveSerial func() (string, error)
}

BugreportRunner holds the collection and serial resolution behind fields so a test can record the call without a device. The defaults reach adb / simctl.

func (BugreportRunner) Run

func (runner BugreportRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type CheckSyntaxRunner

type CheckSyntaxRunner struct {
	Checker Checker
	Getwd   func() (string, error)
}

CheckSyntaxRunner enforces the command's exact stdout/stderr/exit contract.

func (CheckSyntaxRunner) Run

func (runner CheckSyntaxRunner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int

type Checker

type Checker interface {
	Check(context.Context, Source) error
}

Checker owns parse plus recursive capability preflight for one selected root. A successful return is the sole authority for printing OK.

type CryptoRandom

type CryptoRandom struct{}

CryptoRandom is the process random source, shared by the JS faker binding and the random-input commands so there is one notion of randomness.

func (CryptoRandom) Intn

func (CryptoRandom) Intn(n int) int

func (CryptoRandom) Read

func (CryptoRandom) Read(p []byte) (int, error)

type DeviceSession

type DeviceSession struct {
	Driver device.Driver
	// OutputDirectory receives screenshots and other run artifacts.
	OutputDirectory string
	// BaseDirectory resolves flow resources such as runScript files.
	BaseDirectory string
	// Clock is optional; a nil clock uses the wall clock. Tests supply their
	// own so a flow's waits do not take real time.
	Clock engine.Clock
	// ExecutionID is optional; a blank one is derived from the run's own start.
	ExecutionID string
	// Shard is which share of the run this session executes. It is what the
	// reserved FLOWBATON_SHARD_* and FLOWBATON_DEVICE_UDID variables are built
	// from, so a flow can report where it ran.
	Shard Shard
}

DeviceSession runs a prepared program against one device.

It exists to hold the assembly of engine.Dependencies in one place. That assembly is the part where a missing service is invisible: the engine takes every boundary as an interface, so a nil one compiles and then fails inside a flow, where it reads as a flow failure rather than a wiring mistake.

func NewDeviceSession

func NewDeviceSession(options TestOptions, shard Shard) (DeviceSession, error)

NewDeviceSession builds a session for one shard, or explains why it cannot.

func (DeviceSession) Execute

func (session DeviceSession) Execute(
	ctx context.Context,
	program *engine.Program,
	options TestOptions,
) ([]engine.FlowResult, error)

Execute opens the device, runs every selected root, and closes the device even when a flow fails.

type DriverRecordingController

type DriverRecordingController struct {
	// contains filtered or unexported fields
}

DriverRecordingController holds the capture id between the two commands.

func NewDriverRecordingController

func NewDriverRecordingController(
	driver screenRecordingDriver, directory string,
) *DriverRecordingController

func (*DriverRecordingController) Start

func (controller *DriverRecordingController) Start(
	ctx context.Context, request engine.RecordingStartRequest,
) error

func (*DriverRecordingController) Stop

func (controller *DriverRecordingController) Stop(ctx context.Context) ([]device.Artifact, error)

type DriverSetupRunner

type DriverSetupRunner struct {
	Build func(ctx context.Context, platform, teamID string) error
}

DriverSetupRunner builds the driver. The build is a field so a test can record the invocation without running xcodebuild.

func (DriverSetupRunner) Run

func (runner DriverSetupRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type GenerateCompletionRunner

type GenerateCompletionRunner struct{}

GenerateCompletionRunner emits a shell completion script. Pure: no device, no I/O beyond stdout.

func (GenerateCompletionRunner) Run

func (GenerateCompletionRunner) Run(_ context.Context, args []string, stdout, stderr io.Writer) int

type HierarchyRunner

type HierarchyRunner struct {
	Fetch func(ctx context.Context, platform, udid string, appIDs []string) (device.TreeNode, error)
}

HierarchyRunner holds the tree fetch behind a field so a test can stand in a known tree without a device. The default opens the real driver, snapshots, and closes it.

func (HierarchyRunner) Run

func (runner HierarchyRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type ImageChecker

type ImageChecker struct{}

ImageChecker forwards to the shared check rather than repeating it.

func (ImageChecker) Check

type InputGenerator

type InputGenerator struct {
	// contains filtered or unexported fields
}

InputGenerator supplies values for the inputRandom* commands.

func NewInputGenerator

func NewInputGenerator() *InputGenerator

NewInputGenerator builds a generator over the process random source.

func (*InputGenerator) Generate

func (generator *InputGenerator) Generate(
	_ context.Context,
	request engine.InputRequest,
) (string, error)

type ListDevicesRunner

type ListDevicesRunner struct {
	IOS     func(context.Context) ([]ios.Device, error)
	Android func(context.Context) ([]android.Device, error)
}

ListDevicesRunner holds the two listing calls behind fields so a test can stand in a fake without a simulator or an attached phone. The defaults reach real tooling.

func (ListDevicesRunner) Run

func (runner ListDevicesRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

Run lists devices for the selected platform(s). With no -p it lists both, and a platform whose tooling is absent is a note rather than a failure — the command is most wanted exactly on a machine that has only one SDK. With an explicit -p, that platform's error is the failure the operator asked to hear.

type MCPRunner

type MCPRunner struct {
	Checker     Checker
	ListDevices ListDevicesRunner
	Hierarchy   HierarchyRunner
	Query       QueryRunner
}

MCPRunner builds and serves the flowbaton MCP server over stdio. Its dependencies are fields so a test can drive the same server through an in-memory transport.

func (MCPRunner) Run

func (runner MCPRunner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int

type ParserChecker

type ParserChecker struct {
	Preflight Preflight
}

ParserChecker parses the selected root before invoking recursive preflight. A missing preflight fails closed and can never produce a successful check.

func NewParserChecker

func NewParserChecker() ParserChecker

NewParserChecker returns the production syntax checker. Parsing is followed by recursive capability analysis before a syntax check can succeed.

func (ParserChecker) Check

func (checker ParserChecker) Check(ctx context.Context, source Source) error

type Preflight

type Preflight interface {
	Check(context.Context, Source, model.Flow) error
}

Preflight is the narrow integration boundary required from the recursive graph/capability package. It receives the parsed root and the cwd resolution context, and must remain side-effect-free.

type QueryRunner

type QueryRunner struct {
	Fetch func(ctx context.Context, platform, udid, appID, expression string) ([]device.TreeNode, error)
}

QueryRunner holds the query behind a field so a test can match without a device. The default opens the real driver, queries, and closes it.

func (QueryRunner) Run

func (runner QueryRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type RecordRunner

type RecordRunner struct {
	NewSession func(Shard, TestOptions) (TestSession, error)
}

RecordRunner records one flow. The session hook is the same one TestRunner takes, so a test drives it without a device.

func (RecordRunner) Run

func (runner RecordRunner) Run(
	ctx context.Context, args []string, stdout, stderr io.Writer,
) int

type ResourceReader

type ResourceReader struct {
	// contains filtered or unexported fields
}

ResourceReader reads flow resources confined to one base directory.

func NewResourceReader

func NewResourceReader(baseDirectory string) *ResourceReader

func (*ResourceReader) Read

type SessionFactory

type SessionFactory func(Shard, TestOptions) (TestSession, error)

SessionFactory builds the session for one shard.

type Shard

type Shard struct {
	// Index is 0-based; the human-facing number is Index+1.
	Index int
	// Roots are the flow paths this shard runs, in the plan's own order.
	Roots []string
	// Device is the udid this shard runs on. It is blank for an unsharded run
	// with no --device, so the driver keeps saying which flag is missing.
	Device string
	// OutputDirectory is where this shard's artifacts go. The runner fills it
	// in after planning; planning itself has no opinion about the filesystem.
	OutputDirectory string
	// DriverPort is the loopback port this shard's driver talks to. Shard 1
	// keeps the contract's port; later shards get one each, because two shards
	// on one port drive the same runner.
	DriverPort int
}

Shard is one device's share of a run.

func PlanShards

func PlanShards(options TestOptions, plan workspace.Plan) ([]Shard, error)

PlanShards divides a discovered plan into per-device shards.

func (Shard) Count

func (shard Shard) Count() int

Count is the human-facing shard number, matching the `shard-N` directories.

type Source

type Source struct {
	Name    string
	BaseDir string
	Data    []byte
}

Source is one selected syntax-check root. BaseDir is always the invocation working directory; this makes links from stdin resolve from cwd without changing their user-facing source name from "-".

func (source Source) ResolveLink(path string) string

ResolveLink resolves a parser-produced link against the invocation working directory. Absolute links remain absolute.

type StartDeviceRunner

type StartDeviceRunner struct {
	// Boot boots an existing iOS simulator by udid (default: simctl boot).
	Boot func(ctx context.Context, platform, udid string) error
	// ListAVDs enumerates installed Android AVD names (default: emulator -list-avds).
	ListAVDs func(ctx context.Context) ([]string, error)
	// LaunchAVD launches an Android AVD by name (default: emulator -avd <name>).
	LaunchAVD func(ctx context.Context, avd string) error
	// CreateSim creates a new iOS simulator and returns its udid (default: simctl create).
	CreateSim func(ctx context.Context, osVersion, deviceLocale string) (string, error)
	// CreateAVD creates a new Android AVD and returns its name (default: avdmanager create).
	CreateAVD func(ctx context.Context, osVersion, deviceLocale string) (string, error)
}

StartDeviceRunner holds each external operation behind a field so tests can record calls without a real device. The defaults reach simctl/emulator.

func (StartDeviceRunner) Run

func (runner StartDeviceRunner) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

type TestOptions

type TestOptions struct {
	Roots []string

	ConfigPath  string
	Env         map[string]string
	IncludeTags []string
	ExcludeTags []string

	Format        string
	TestSuiteName string
	Output        string

	DebugOutput        string
	TestOutputDir      string
	FlattenDebugOutput bool
	Continuous         bool
	Headless           bool
	Analyze            bool
	ReinstallDriver    bool
	ScreenSize         string
	APIURL             string
	APIKey             string
	AppleTeamID        string
	Platform           string
	Devices            []string
	ShardSplit         int
	ShardAll           int

	// RecordTo is where `record` wants the video. It is NOT parsed from a
	// command line: the `test` command has no such flag. RecordRunner sets it.
	RecordTo string

	// SequencedRoots and ContinueOnFailure come from the workspace's
	// executionOrder, not from argv. They decide whether a failed flow ends
	// the suite; see engine.Dependencies for the rule.
	SequencedRoots    int
	ContinueOnFailure bool
	// contains filtered or unexported fields
}

TestOptions is one parsed `test` command line.

func ParseTestOptions

func ParseTestOptions(args []string) (TestOptions, error)

ParseTestOptions reads a `test` command line.

Flags and positionals may interleave. Boolean flags never consume the following argument — doing so would silently drop a flow from the run, which is the worst way for a suite to pass.

type TestRunner

type TestRunner struct {
	// NewSession acquires one shard's device and executes its flows. It is
	// injected because no production session can be built without a device; a
	// nil factory falls back to the real one.
	NewSession SessionFactory
	Loader     capability.FlowLoader
	// Clock is optional; nil uses the wall clock. It fixes the run's own notion
	// of now, which both the default output directory and the report timestamp
	// read, so two identical runs render identical bytes.
	Clock engine.Clock
	// Environ is optional; nil reads the process environment. It supplies the
	// shell's FLOWBATON_ variables, per specs/01-core-engine.md:101.
	Environ func() []string
	// AllocatePort is optional; nil asks the operating system for an ephemeral
	// port. Injected so a test can pin the ports a sharded run picks.
	AllocatePort func() (int, error)
	// PollInterval is how often continuous mode re-stamps its watch set; zero
	// uses defaultPollInterval. Injected so a test does not wait on it.
	PollInterval time.Duration
	// RecordTo is set by RecordRunner, never by argv. See TestOptions.RecordTo.
	RecordTo string
}

TestRunner discovers flows, preflights them, splits them into shards, and runs each shard on its own device.

Discovery, preflight, and shard planning all run BEFORE anything touches a device. That order is the point: a config error, an unknown command, a tag filter that selects nothing, or a shard count with no devices behind it are all knowable without a simulator, and finding them after devices have been acquired wastes the slowest part of a run and reports a setup mistake as a flow failure.

func (TestRunner) Run

func (runner TestRunner) Run(
	ctx context.Context,
	args []string,
	stdout io.Writer,
	stderr io.Writer,
) int

Run executes one `test` invocation and returns its process exit code.

func (TestRunner) RunOptions

func (runner TestRunner) RunOptions(
	ctx context.Context,
	options TestOptions,
	stdout io.Writer,
	stderr io.Writer,
) int

RunOptions is Run past its argument parsing. `record` uses it: it has to read the positionals itself (a flow plus an optional output file), so it parses the same options and then hands them over rather than rebuilding a command line for this to parse a second time.

type TestSession

type TestSession interface {
	Execute(context.Context, *engine.Program, TestOptions) ([]engine.FlowResult, error)
}

TestSession is the boundary between the host pipeline and a device.

type UnsupportedRecordingController

type UnsupportedRecordingController struct{}

UnsupportedRecordingController refuses both halves of the recording lifecycle.

It is what a driver without the stop half gets. Reporting success and producing no file would tell a run it has a recording it does not have.

func (UnsupportedRecordingController) Start

func (UnsupportedRecordingController) Stop

type UsageError

type UsageError struct {
	Message string
}

UsageError marks a command line that could not be understood. It is the only thing that exits 2; everything else exits 1.

func (*UsageError) Error

func (err *UsageError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL