Documentation
¶
Overview ¶
Package monogatari is the story library for apps built on ochazuke: the story format, the runner and driver, recordings and trimming, explore, and the host an app mounts to build its own story tester.
An app links monogatari only in its tester build, behind the build tag "monogatari"; a normal build of the app links none of it. ochazuke's own o-story is one more host of this library.
A story is data (a script of events and what must hold afterwards, plus the app, fixture and seed that build its starting state); Library.Build turns it into a live runtime and model through the app's fixture, a Driver plays events against it headless, and a Recorder turns the messages of a live session into the same events. State changes only through events; a frame is a pure function of the story and its events. Stories never read production state: a fixture gets only what Env gives it.
Index ¶
- Constants
- func Check(ss *Session, e Expect) []string
- func Golden(frame string) string
- func Lookup(apps []App, s *Story) (*App, *Fixture, error)
- func Slug(name string) string
- func Sort(stories []*Story)
- func Unbatch(t Tagged) (tea.Cmd, bool)
- func Wrap(gen int, cmd tea.Cmd) tea.Cmd
- func WriteStory(dir string, s *Story) (string, error)
- type App
- type Built
- type CheckSpec
- type Driver
- type Edge
- type Entry
- type Env
- type Event
- type Expect
- type Expects
- type Fixture
- type Library
- func (lib Library) Build(s *Story, opt Options) (*Session, error)
- func (lib Library) Replay(rec *Recording, expect Expects) Result
- func (lib Library) Resolve(s *Story) (*Story, []Event, error)
- func (lib Library) Run(s *Story, opt RunOptions) (res Result)
- func (lib Library) Subject(s *Story) explore.Subject
- func (lib Library) Trim(base string, initial *Story, events []Event, t TrimSpec) (*Story, []string, error)
- type Minimum
- type Options
- type Ran
- type Recorder
- type Recording
- type Result
- type RunOptions
- type Session
- func (ss *Session) Close()
- func (ss *Session) Cursor() *tea.Cursor
- func (ss *Session) Emit() bool
- func (ss *Session) Frame() string
- func (ss *Session) HasPane() bool
- func (ss *Session) Rebuild(restart bool) (*Session, error)
- func (ss *Session) Snapshot() (Snapshot, error)
- func (ss *Session) Start() tea.Cmd
- func (ss *Session) Update(msg tea.Msg) tea.Cmd
- func (ss *Session) View() tea.View
- func (ss *Session) Writes() []string
- type Snapshot
- type Step
- type Store
- func (s Store) Delete(ref string) (string, error)
- func (s Store) History() ([]Entry, error)
- func (s Store) Keep(n int, name string) (string, error)
- func (s Store) Load(ref string) (*Entry, error)
- func (s Store) Move(path, name string) (string, error)
- func (s Store) Push(rec *Recording) (string, error)
- func (s Store) Save(rec *Recording, name string) (string, error)
- func (s Store) Saved() ([]Entry, error)
- type Story
- func (s *Story) Field(name string, v any) (bool, error)
- func (s *Story) GoldenPath() string
- func (s Story) MarshalJSON() ([]byte, error)
- func (s *Story) ReadGolden() ([]byte, error)
- func (s *Story) Size() (int, int)
- func (s *Story) UnmarshalJSON(b []byte) error
- func (s *Story) WriteGolden(frame string) error
- type Tagged
- type TrimSpec
Constants ¶
const HistorySize = 5
HistorySize is how many unnamed recordings the ring keeps.
Variables ¶
This section is empty.
Functions ¶
func Golden ¶
Golden is a frame as a golden file holds it: each row stripped of escapes behind a gutter that reads "R|" when the row has full-strength reverse video, "r|" when its only reverse video is dimmed (faint: an unfocused list's selected row, a highlight beneath a modal) and " |" otherwise, so goldens pin the highlight without colour. A highlighted row holding a table's current cell (reverse and underline together) reads "C|", dimmed "c|".
Types ¶
type App ¶
type App struct {
Name string
Summary string
// Fixtures are the app's named starting states; the first is the
// default for a story that names none.
Fixtures []Fixture
// Fields are the top-level story fields the app reads beyond the
// story format's own (the demo's page and pages, Story.Fields). A
// story with a field its app does not declare fails to build.
Fields []string
// Expected reports an operation error the app means (a refusal of its
// fakes), which the explorer must not report; nil is none.
Expected func(op, err string) bool
}
App is an application stories can build; every host registers its own (Library.Apps). monogatari has no built-in app.
type Built ¶
type Built struct {
Model *ochazuke.Model
// Program is the app's own tea.Model when it wraps the ochazuke
// model's Update and View; nil drives Model.
Program tea.Model
// Init runs Program's (or Model's) Init before the first event; false
// runs only Runtime.PendingCommands.
Init bool
// Start runs once the host has loaded the story's config, hung its
// checks, sized the session and drawn the first frame (jobs a story
// starts, a scroll offset).
Start func() error
// Close releases the fixture's fakes; the host removes the temp dirs.
Close func()
// Emit makes every live fake pane print a line (the output tick),
// reporting whether one was live; HasPane reports one is live.
Emit func() bool
HasPane func() bool
// Writes is what the app wrote, for the writes expect and dirty
// checks; nil is none.
Writes func() []string
// Snapshot bakes the session's current state into a story's app data,
// for trim --snapshot; nil cannot.
Snapshot func() (Snapshot, error)
}
Built is what a fixture made.
type CheckSpec ¶
type CheckSpec struct {
Verb string `json:"verb,omitempty"`
Name string `json:"name,omitempty"`
Ask string `json:"ask,omitempty"`
Refuse string `json:"refuse,omitempty"`
Dirty bool `json:"dirty,omitempty"`
}
CheckSpec is one verb check: on Verb (default app.quit), Ask or Refuse while the page's first editable tree has unwritten changes (Dirty: any write since the story started), else always (Dirty false).
type Driver ¶
type Driver struct {
S *Session
Opt Options
Wait time.Duration // how long a job or tick event waits for its message
Quit bool // the script quit the story (q, ctrl+c)
// Trace is one Step per event played: the mode before and after and
// the top-level noun.verb requests the event ran.
Trace []Step
// contains filtered or unexported fields
}
Driver plays events against a session with no terminal: commands run on goroutines and their messages wait in a pool; a job or tick event takes the first matching message from the pool (waiting for it) and delivers it, so nothing reaches the model except through an event, in order. The model runs on the driver's fake clock (Model.Now), which stands still between events; a linger tick advances it to the timer's deadline and delivers the timer's message at once (Model.JobLingerTick), so a story never waits JobLinger.
func (*Driver) Arrived ¶
Arrived delivers every pooled message of this generation that is an event (a job's completion, a pane frame, a framework timer) in arrival order, each as a traced step, and returns the events it played. A live recorder calls it between keys so the session follows its jobs without a script naming them; a scripted run never needs it.
type Edge ¶
type Edge struct {
From, To string
Via string // noun.verb (joined when a step ran several), or the row/tick that did it
}
Edge is a mode change one step made.
type Entry ¶
Entry is one recording on disk; N is its place in the ring, 1 newest (0 for a saved one).
type Env ¶
type Env struct {
Story *Story
// Raw is the whole story document (Story's own fields and Fields).
Raw json.RawMessage
Seed json.RawMessage
// StateDir is the session's state directory (Story.State; kept across
// a restart), ConfigDir the temp directory the story's config files
// are written to (Story.Config), FilesDir its file tree (Story.Files);
// each "" when the story has none.
StateDir, ConfigDir, FilesDir string
// Now is the session's clock: the driver's fake clock once a driver
// runs the session, the epoch before.
Now func() time.Time
Width, Height int
Options Options
// Reset asks the host to rebuild the session from its story after the
// event being played (a reset verb).
Reset func()
// contains filtered or unexported fields
}
Env is everything a fixture may use: the story, its app data and the temp directories the host made for it.
func (Env) Field ¶
Field decodes the story's app field name into v strictly, reporting whether the story has it.
func (Env) NewRuntime ¶
NewRuntime makes a sandboxed runtime and model for a fixture that builds its app from scratch: user config off, state only in StateDir, the story's settings declared and its values set, a headless foreground, and the story's theme.
type Event ¶
type Event struct {
T int64 `json:"t,omitempty"`
Key string `json:"key,omitempty"`
Type string `json:"type,omitempty"`
Paste string `json:"paste,omitempty"`
Job string `json:"job,omitempty"` // noun.verb of the job that delivered
State string `json:"state,omitempty"` // done or failed
Tick string `json:"tick,omitempty"` // linger, binding, terminal (a pane frame), output (fake output line), debounce (a tree line's, a form's or a completion's pause) or segment (the status segments' next refresh, answered)
// Restart quits the session and builds it again from the story with
// the same state directory (State), as a user reopening the app.
Restart bool `json:"restart,omitempty"`
}
Event is one action: a key press, typed text, a paste, a job delivering, or a framework timer firing. T is milliseconds since the session started (recordings only; scripts leave it 0).
func Classify ¶
Classify names the event a message is, before it is delivered: a key, a paste, a current job's completion, a pane frame, a framework timer. Other messages (window size, colours) are not events.
func (Event) MarshalJSON ¶
MarshalJSON writes an untimed key press as a bare string.
func (*Event) UnmarshalJSON ¶
UnmarshalJSON reads a bare string as a key press.
type Expect ¶
type Expect struct {
After int `json:"after,omitempty"`
Focus string `json:"focus,omitempty"`
Mode string `json:"mode,omitempty"`
Modal *string `json:"modal,omitempty"` // top modal instance, "" for none
Quit *bool `json:"quit,omitempty"` // Runtime.Quit: app.quit ran
Page string `json:"page,omitempty"`
// Shift is the current page's pane divider (Runtime.PaneShift).
Shift *int `json:"shift,omitempty"`
Visible *bool `json:"visible,omitempty"` // the focused element's label is on screen
// Highlighted counts rows holding a full-strength highlight (reverse
// video not under faint); Dimmed counts rows holding a dimmed one (an
// unfocused page list's selected row, a highlight beneath a modal).
// Dimmed is read in the mono theme, where a dim is the faint attribute.
Highlighted *int `json:"highlighted,omitempty"`
Dimmed *int `json:"dimmed,omitempty"`
// HighlightedText is text every highlighted row holds (the selected row's label).
HighlightedText string `json:"highlightedText,omitempty"`
// Cell is the text of a table's current cell (drawn reverse and
// underlined inside its row, dimmed or not); "" for none.
Cell *string `json:"cell,omitempty"`
// Badge is the mode badge at the right of the title row (full
// chrome): its whole text, padded by a cell on each side, followed by
// the end of the row or a " · " (the focus display; a recording's
// "· REC @q" too, so expect the whole badge while recording) (R2.10).
Badge string `json:"badge,omitempty"`
Contains []string `json:"contains,omitempty"`
Lacks []string `json:"lacks,omitempty"`
Order []string `json:"order,omitempty"` // substrings that appear on screen in this order
Ticker []string `json:"ticker,omitempty"`
NoTicker bool `json:"noTicker,omitempty"`
// Segments are the status segments the header rule draws
// (Model.StatusSegments); NoSegments expects none.
Segments []string `json:"segments,omitempty"`
NoSegments bool `json:"noSegments,omitempty"`
Notices []string `json:"notices,omitempty"`
NoNotices bool `json:"noNotices,omitempty"`
// Colored maps on-screen text to a "#RRGGBB" foreground some row holding
// that text draws in; NoColor lists foregrounds no cell may use.
Colored map[string]string `json:"colored,omitempty"`
NoColor []string `json:"noColor,omitempty"`
// Styled maps on-screen text to whether it starts a run drawn in Hex
// ("#RRGGBB"): the SGR right before the text sets that foreground,
// cell-exact where Colored is row-wide. "!#RRGGBB" expects any other.
Styled map[string]string `json:"styled,omitempty"`
// Attrs maps on-screen text to the attributes its first cell is drawn
// with, followed along its row's SGR state: a comma list of bold,
// faint, reverse, underline, fg (any foreground colour) and bg (any
// background colour), each
// "-name" for absent. "bold,-fg" reads a mono tone.
Attrs map[string]string `json:"attrs,omitempty"`
// Writes is every change the page's editable trees, record editors and
// Markdown documents wrote, in order (TreeChange.String,
// RecordWrite.String, "toggle LINE [x] TEXT"); [] expects none.
Writes *[]string `json:"writes,omitempty"`
// Register is the unnamed register's text (a yank).
Register *string `json:"register,omitempty"`
}
Expect is what must hold after a step (After events) or at the end (After 0). Every field is optional.
type Expects ¶
type Expects []Expect
Expects is one final Expect or a list of per-step ones.
func (Expects) MarshalJSON ¶
func (*Expects) UnmarshalJSON ¶
type Fixture ¶
type Fixture struct {
Name, Summary string
// Build makes the app's runtime for a story. It reads only what Env
// gives it: nothing else on the machine.
Build func(Env) (Built, error)
}
Fixture is one named starting state of an app.
type Library ¶
Library is every story a run can name (embedded and the user's), the recordings store a trimmed story's base may name, and the apps whose fixtures build the stories (the first is the default; there is no built-in app).
func (Library) Build ¶
Build makes a session of a story through its app's fixture. A trimmed story's base is resolved by the caller (Resolve) before Build.
func (Library) Replay ¶
Replay plays a recording headless: its events are the script, and only expect (the recording's own when nil) is asserted.
func (Library) Resolve ¶
Resolve turns a trimmed story into the base's initial story with this story's script and expectations, plus the base events to replay first. A base is "story:NAME" (its script) or "recording:NAME" (a saved recording's events).
func (Library) Run ¶
func (lib Library) Run(s *Story, opt RunOptions) (res Result)
Run plays a story headless and checks it: each per-step expect after its step, the final expect at the end, else the golden frame.
func (Library) Subject ¶
Subject explores a story's application: every replay builds the story afresh through its app's fixture (its fakes included), drives the app's own program when the fixture has one, and closes it after. The app's Expected refusals are not findings.
func (Library) Trim ¶
func (lib Library) Trim(base string, initial *Story, events []Event, t TrimSpec) (*Story, []string, error)
Trim writes a story of events[from..to] of the source. Without Snapshot it names its base ("recording:NAME" or "story:NAME") and the runner replays events[:from] headless first; with Snapshot it plays them now and bakes the state they reach (lists, rows, selection, marks, focus, page offset) into the story's pages, so the story stands alone. The expect is the state observed at the end of the window: edit it to the correct state to make the story fail until the bug is fixed.
type Minimum ¶
Minimum is what a set of stories exercises: per mode and noun the noun.verb requests their keys resolved to (the minimum a component needs to be full-featured; which keys reach them is the app's choice), and the edges between modes with what took them.
type Options ¶
type Options struct {
Mono bool // the mono theme: no colour, as goldens compare
}
Options change how a session renders, never what it does.
type Recorder ¶
type Recorder struct {
Rec Recording
// contains filtered or unexported fields
}
Recorder collects a session's events as they happen.
func NewRecorder ¶
NewRecorder starts a recording of s now.
type Recording ¶
type Recording struct {
Story string `json:"story"`
Started time.Time `json:"started"`
Initial *Story `json:"initial"`
Events []Event `json:"events"`
Expect Expects `json:"expect,omitempty"`
}
Recording is one live session in a story: the initial story and the ordered events (keys, job deliveries, timer ticks) with their times. Replaying the events against the initial story reproduces every frame.
type Result ¶
type Result struct {
Story *Story
Failed []string
Err error
Skipped bool // the story carries a skip ("gap: ..."): it ran but cannot fail
Updated bool // --update wrote the golden
Trace []Step // the window's steps (the base's prefix excluded)
}
Result is one story's outcome.
type RunOptions ¶
type RunOptions struct {
Update bool // rewrite goldens
Golden bool // compare the final frame to a golden when the story has no final expect
}
RunOptions control one run.
type Session ¶
type Session struct {
Story *Story
R *ochazuke.Runtime
M *ochazuke.Model
// Program is the app's tea.Model when it wraps M; nil drives M.
Program tea.Model
// Reset is set by Env.Reset; the host (TUI or Driver) rebuilds.
Reset bool
// StateDir is the state directory of a story with State, "" without;
// Close removes it unless the session is restarting into it.
StateDir string
App *App
Fixture *Fixture
// contains filtered or unexported fields
}
Session is one built story: the runtime, the model, the app's own program, and what the host made for it.
func (*Session) Close ¶
func (ss *Session) Close()
Close releases the fixture's fakes and the session's temp dirs.
func (*Session) Cursor ¶
Cursor is the terminal cursor of the last Frame, relative to the story's area, or nil.
func (*Session) Emit ¶
Emit makes every live fake pane print one more output line (the "output" tick), reporting whether one was live.
func (*Session) Frame ¶
Frame renders the story's area: exactly Width x Height cells of trusted ANSI, by the story's chrome mode.
func (*Session) Rebuild ¶
Rebuild builds the session again from its story, closing this one; a restart keeps the state directory, a reset starts with a new one.
func (*Session) Start ¶
Start is the command that starts the session: the program's Init when the fixture asks for it, else the runtime's pending commands.
func (*Session) Update ¶
Update delivers a message to the app's program (the model when it has none).
type Snapshot ¶
type Snapshot struct {
Fields map[string]json.RawMessage
Seed json.RawMessage
Warn []string
}
Snapshot is a session's current state as story data: the fields and the seed of a story that starts there, and what it cannot carry.
type Step ¶
Step is what one event did: the mode it was read in, the mode after it and the top-level requests it ran (noun.verb in the mode each ran in).
type Store ¶
type Store struct{ Dir string }
Store is a host's data directory: history/ is the ring of the last HistorySize recordings, saved/ the named ones, which the ring never drops.
func DefaultStore ¶
DefaultStore is the store of the host named host (its slug: o-story, mtm-story): $XDG_DATA_HOME/HOST, else ~/.local/share/HOST. Each host has its own ring and saved recordings.
func (Store) Delete ¶
Delete removes a recording by ring number (1 newest) or saved name, returning the file it removed. Deleting a ring entry renumbers the older ones.
func (Store) Push ¶
Push writes a recording into the ring and drops the oldest past HistorySize. It returns the file written.
type Story ¶
type Story struct {
Name string `json:"name"`
Area string `json:"area"`
Skip string `json:"skip,omitempty"` // "gap: ..." marks a known framework gap; the story runs but cannot fail the suite
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Chrome string `json:"chrome,omitempty"` // full (default), none (page body only) or ticker-only
// Theme renders the story in a theme other than mono (adaptive, dark,
// light); Accent ("#RRGGBB") is the Appearance accent override. Goldens
// stay escape-free, so a coloured story asserts colours with colored.
Theme string `json:"theme,omitempty"`
Accent string `json:"accent,omitempty"`
// Themes are colour schemes in the themes.json shape the session adds
// to the built-in themes (Theme may name one); a scheme that does not
// validate is a notice, as in an application.
Themes json.RawMessage `json:"themes,omitempty"`
// App names the application whose fixture builds the session ("" is
// the host's first app); Fixture names one of its starting states (""
// is the app's first); Seed is app-defined data handed to the fixture.
App string `json:"app,omitempty"`
Fixture string `json:"fixture,omitempty"`
Seed json.RawMessage `json:"seed,omitempty"`
// Base and Start make a trimmed story: replay the base's events before
// Start headless, then play Script as the window under test.
Base string `json:"base,omitempty"`
Start int `json:"start,omitempty"`
Script []Event `json:"script,omitempty"`
// Settings are declared on the session's runtime; with no config
// directory (stories read no user files) their values last the session.
Settings []ochazuke.Setting `json:"settings,omitempty"`
// Values are setting values the session starts with, set as
// settings.json would hold them (Settings.Set; with no config
// directory they last the session): the saved layer.
Values map[string]any `json:"values,omitempty"`
// Session are session values set at the start, above Values (the
// session started as Model.Init starts it, then Settings.SetSession):
// what a :set line leaves unwritten.
Session map[string]any `json:"session,omitempty"`
// State gives the session a state directory of its own (a temp dir,
// removed when the session ends), kept across a restart event, so a
// story shows what the application remembers (pane dividers).
State bool `json:"state,omitempty"`
// Config is the user's config files the session starts with, by file
// name: written into a temp config directory of its own (removed at
// the end); actions.json and then bindings.json are loaded the way
// Model.Init loads them, after the pages are built. A problem a load
// reports is a notice.
Config map[string]json.RawMessage `json:"config,omitempty"`
// Checks hang verb checks on the session's verbs (Runtime.AddVerbCheck;
// app.quit is BeforeQuit's), after the pages are built.
Checks []CheckSpec `json:"checks,omitempty"`
// Files is a file tree the session makes in a temp directory of its
// own (removed at the end): paths relative to it, a trailing / for a
// folder. {{files}} in a record field's base or value names it, and
// writes show it as {{files}}.
Files []string `json:"files,omitempty"`
Expect Expects `json:"expect,omitempty"`
// Fields are the story's other top-level fields, the ones an app
// reads (App.Fields: the demo's page and pages), kept as written. A
// field the story's app does not declare fails the build.
Fields map[string]json.RawMessage `json:"-"`
// Path is where the file was loaded from (not part of the data); rel
// is the path inside fsys, disk the directory fsys reads when it is on
// disk (goldens are written there).
Path string `json:"-"`
// contains filtered or unexported fields
}
Story is one scenario file.
func LoadDir ¶
LoadDir reads the stories under a directory on disk; a missing directory is no stories.
func (*Story) Field ¶
Field decodes the top-level field name into v strictly, reporting whether the story has it.
func (*Story) GoldenPath ¶
GoldenPath is the golden frame file beside the story.
func (Story) MarshalJSON ¶
MarshalJSON writes Story's own fields, then Fields in name order.
func (*Story) ReadGolden ¶
ReadGolden reads the golden frame, or reports fs.ErrNotExist.
func (*Story) UnmarshalJSON ¶
UnmarshalJSON reads Story's own fields strictly and keeps every other top-level field in Fields, for the story's app to read.
func (*Story) WriteGolden ¶
WriteGolden writes the golden frame beside a story loaded from disk.
type Tagged ¶
Tagged is a message from a session's command, tagged with the session's generation so a message of a session that was reset or left never reaches its successor (job ids restart with each runtime).
type TrimSpec ¶
type TrimSpec struct {
From, To int // inclusive event indexes (0-based); -1 = unset
At int // the failing event; From/To default to At-3 and At+2; -1 = unset
As string // the new story's name
Snapshot bool // bake the before-state into the story's app data (Built.Snapshot) instead of naming a base
}
TrimSpec chooses a window of a recording's (or story's) events.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package explore explores an ochazuke application's interaction states by pressing keys, checks properties in every state it reaches, and reports bad and impossible states and actions with the shortest key path to each.
|
Package explore explores an ochazuke application's interaction states by pressing keys, checks properties in every state it reaches, and reports bad and impossible states and actions with the shortest key path to each. |
|
Package host is a monogatari host: the story tester an app mounts.
|
Package host is a monogatari host: the story tester an app mounts. |