Documentation ¶
Overview ¶
Package mimic provides a utility for interacting with console or terminal based applications.
A mimic/Mimic internally constructs two pseudo-terminals: one wrapping go-expect, and another construct of creak/pty. This allows for either stream-based or view-based inspection of strings/patterns.
The key difference between the two is that stream-based inspections provided by Mimic.ExpectString and Mimic.ExpectPattern will wait for a configurable amount of time for any text matching the criteria, then _fail_ if no match is found. The search criteria passed to these functions is evaluated repeatedly as bytes are written to your output stream. Keep this in mind, as very complex patterns can be slow. The underlying views are raw pty, and the output is therefore not formatted as it would be within a terminal.
The view-based inspections provided by Mimic.ContainsString and Mimic.ContainsPattern, on the other hand, will wait for the bound output stream to complete processing before applying the search criteria to the entire formatted view. This takes configurable terminal columns/rows into account. These default to a large standard of 132 columns and 24 rows. Internally, this is implemented via github.com/hinshun/vt10x.
Usage ¶
A mimic value implements io.ReadWriteCloser and also satisfies the following interfaces:
type fileWriter interface { io.Writer Fd() uintptr } type fileReader interface { io.Reader Fd() uintptr }
This allows Mimic values to be used in place of Stdin/Stdout/Stderr in most scenarios, including implementations using github.com/AlecAivazis/survey/v2. For example:
package main import ( "fmt" "os" "github.com/AlecAivazis/survey/v2" "github.com/jimschubert/mimic" ) func main() { console, _ := mimic.NewMimic() answers := struct { Name string Age int }{} go func() { // errors ignored for brevity console.ExpectString("What is your name?") console.WriteString("Tom\n") console.ExpectString("How old are you?") console.WriteString("20\n") console.ExpectString("Tom", "20") if !console.ContainsString("What is your name?", "How old are you?", "Tom", "20") { panic("My answers weren't displayed!") } _ = console.NoMoreExpectations() }() _ = survey.Ask([]*survey.Question{ {Name: "name", Prompt: &survey.Input{Message: "What is your name?"}}, {Name: "age", Prompt: &survey.Input{Message: "How old are you?"}}, }, &answers, survey.WithStdio(console.Tty(), console.Tty(), console.Tty()), ) fmt.Fprintf(os.Stdout, "%s is %d.\n", answers.Name, answers.Age) }
Notice in the above example that all expectations should be invoked asynchronously from the thread being instrumented.
Testing ¶
Mimic provides a Suite based on github.com/stretchr/testify/suite which allows creation of a new mimic per test, or a suite-level mimic can be created for more advanced scenarios. Embed suite.Suite into a test struct, then add Test* functions to implement your tests. Follow testify's documentation for more. Here's an slimmed-down example from mimic's own tests:
package suite import ( "context" "io" "strings" "testing" "time" "github.com/jimschubert/mimic" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) type MyTests struct { Suite suiteRuntimeDuration time.Duration } func (m *MyTests) SetupSuite() { assert.Greaterf(m.T(), m.suiteRuntimeDuration, 0*time.Second, "Suite runtime must be marked as more than 0 units of time") } func (m *MyTests) TestMimicWriteRead() { m.T().Log("Invoked TestMimicWithOptions") terminalWidth := 80 wrapLength := 20 console, err := m.Mimic( mimic.WithIdleDuration(25*time.Millisecond), mimic.WithIdleTimeout(1*time.Second), mimic.WithSize(24, terminalWidth), ) assert.NoError(m.T(), err, "Standard invocation with options should not produce an error") assert.NotNil(m.T(), console, "Mimic instance must should not be nil on errorless construction") character := "X" fullWriteWidth := terminalWidth + wrapLength full := strings.Repeat(character, fullWriteWidth) written, err := console.WriteString(full) assert.NoError(m.T(), err, "pty should have allowed the write!") assert.Equal(m.T(), written, fullWriteWidth, "pty should have written all bytes!") assert.NoError(m.T(), console.ExpectString(full), "Emulated terminal should be %d columns, not %d columns as the written string", terminalWidth, fullWriteWidth) assert.Error(m.T(), console.ExpectString(strings.Repeat(character, terminalWidth+1)), "Emulated terminal should be %d columns, but found %d characters", terminalWidth, terminalWidth+1) assert.Error(m.T(), console.ExpectString(strings.Repeat(character, terminalWidth)), "Emulated terminal should have wrapped text at %d columns", terminalWidth) assert.False(m.T(), console.ContainsString(full), "underlying terminal is expected to wrap") assert.True(m.T(), console.ContainsString(strings.Repeat(character, terminalWidth)+"\n"+strings.Repeat(character, wrapLength)), "underlying terminal is expected to wrap") } func TestMimicOperationsSuite(t *testing.T) { test := new(MyTests) test.suiteRuntimeDuration = 30 * time.Second test.Init(WithMaxRuntime(test.suiteRuntimeDuration)) suite.Run(t, test) }
Index ¶
- Constants
- type Experimental
- type Mimic
- func (m *Mimic) Close() (err error)
- func (m *Mimic) ContainsPattern(pattern ...string) bool
- func (m *Mimic) ContainsString(str ...string) bool
- func (m *Mimic) ExpectPattern(pattern ...string) error
- func (m *Mimic) ExpectString(str ...string) error
- func (m *Mimic) Fd() uintptr
- func (m *Mimic) Flush() error
- func (m *Mimic) NoMoreExpectations() error
- func (m *Mimic) Read(p []byte) (n int, err error)
- func (m *Mimic) Tty() *os.File
- func (m *Mimic) WaitForIdle(ctx context.Context) error
- func (m *Mimic) Write(b []byte) (int, error)
- func (m *Mimic) WriteString(str string) (int, error)
- type Option
- type PatternError
- type Viewer
Examples ¶
Constants ¶
const ( // DefaultColumns for the underlying view-based terminal's column count (i.e. width) DefaultColumns = 132 // DefaultRows for the underlying view-based terminal's row count (i.e. height) DefaultRows = 24 // DefaultIdleTimeout when the underlying terminal is idle (i.e. fails to match an expectation), used by functions // such as Mimic.ExpectString, Mimic.ContainsString, Mimic.ExpectPattern, and Mimic.ContainsPattern DefaultIdleTimeout = 250 * time.Millisecond // DefaultFlushTimeout for mimic's flush operation. Mimic will invoke flush only if there are outstanding operations // from Mimic.Write or Mimic.WriteString. DefaultFlushTimeout = 25 * time.Millisecond // DefaultIdleDuration for mimic to consider the terminal idle via Mimic.WaitForIdle. DefaultIdleDuration = 100 * time.Millisecond )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Experimental ¶ added in v0.0.3
type Experimental interface { // Console provides access to the underlying expect.Console Console() (expect.Console, error) // Terminal provides access to the underlying vt10x.Terminal Terminal() (vt10x.Terminal, error) }
An Experimental contract which can be changed or removed at any time. This is intended for use by users for experimentation purposes only.
type Mimic ¶
type Mimic struct { Experimental Experimental // contains filtered or unexported fields }
Mimic is a utility for mimicking operations on a pseudo terminal
func NewMimic ¶
NewMimic creates a Mimic, which emulates a pseudo terminal device and provides utility functions for inputs/assertions/expectations upon it
func (*Mimic) Close ¶
Close causes any underlying emulation to close. Fulfills the io.Closer interface.
func (*Mimic) ContainsPattern ¶
ContainsPattern determines if the emulated terminal's view contains one or more specified patterns. Patterns are evaluated against formatted terminal contents, stripped of ANSI escape characters and trimmed.
func (*Mimic) ContainsString ¶
ContainsString determines if the emulated terminal's view matches specified string. A "view" takes into account terminal row/columns. Terminal contents are stripped of ANSI escape characters and trimmed.
Example ¶
package main import ( "fmt" "time" "github.com/jimschubert/mimic" ) func main() { columns := 26 m, _ := mimic.NewMimic( mimic.WithSize(24, columns), mimic.WithFlushTimeout(75*time.Millisecond), mimic.WithIdleDuration(50*time.Millisecond), ) // create three rows of text… for row := 1; row <= 3; row++ { for i := 'a'; i <= 'z'; i++ { _, _ = m.WriteString(string(i)) } } if m.ContainsString("abcdefghijklmnopqrstuvwxyz") { fmt.Println("Found the alphabet!") } if m.ContainsString("za") { fmt.Println("[Error] Terminal did not wrap!") } formatted := mimic.Viewer{Mimic: m, StripAnsi: true, Trim: true} fmt.Printf("\nFormatted View (%d columns):\n%s\n", columns, formatted.String()) }
Output: Found the alphabet! Formatted View (26 columns): abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz
func (*Mimic) ExpectPattern ¶ added in v0.0.2
ExpectPattern waits for the emulated terminal's view to contain one or more specified patterns
func (*Mimic) ExpectString ¶ added in v0.0.2
ExpectString waits for the emulated terminal's view to contain one or more specified strings
Example ¶
package main import ( "fmt" "strings" "github.com/jimschubert/mimic" ) func main() { columns := 30 m, _ := mimic.NewMimic( mimic.WithSize(24, columns), ) // text is Hi*16 (or, 32 letters); column width is 30 text := strings.Repeat("Hi", 16) _, _ = m.WriteString(text) // Expect the first line (note, no newline expectations) if err := m.ExpectString(strings.Repeat("Hi", 15)); err == nil { fmt.Printf("Found: %s\n\n", strings.Repeat("Hi", 15)) } // Expect the second line (note, no newline expectations) if err := m.ExpectString("Hi"); err != nil { fmt.Println("The text should have wrapped!") } _ = m.NoMoreExpectations() formatted := mimic.Viewer{Mimic: m, StripAnsi: true, Trim: true} fmt.Printf("Formatted View (%d columns):\n%s\n", columns, formatted.String()) }
Output: Found: HiHiHiHiHiHiHiHiHiHiHiHiHiHiHi Formatted View (30 columns): HiHiHiHiHiHiHiHiHiHiHiHiHiHiHi Hi
Example (With_ContainsString) ¶
package main import ( "fmt" "time" "github.com/jimschubert/mimic" ) func main() { columns := 26 m, _ := mimic.NewMimic( mimic.WithSize(24, columns), mimic.WithIdleTimeout(50*time.Millisecond), ) go func() { // create three rows of text… for row := 1; row <= 3; row++ { for i := 'a'; i <= 'z'; i++ { // note we don't write \n here. Formatting defined by column width. _, _ = m.WriteString(string(i)) } } _, _ = m.WriteString("\nDONE.") }() _ = m.ExpectString("DONE.") // force Flush and expect EOF // this can be omitted if you don't want to expect EOF m.NoMoreExpectations() if m.ContainsString("DONE.") { fmt.Printf("Found 'DONE.'\n\n") } if m.ContainsString("za") { fmt.Println("Terminal did not wrap!") } formatted := mimic.Viewer{Mimic: m, StripAnsi: true, Trim: true} fmt.Printf("Formatted View (%d columns):\n%s\n", columns, formatted.String()) }
Output: Found 'DONE.' Formatted View (26 columns): abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz DONE.
func (*Mimic) Flush ¶ added in v0.0.3
Flush (or attempt to flush) any pending writes done via Write or WriteString.
func (*Mimic) NoMoreExpectations ¶ added in v0.0.3
NoMoreExpectations signals the underlying buffer to finish writing bytes to the underlying pseudo-terminal.
func (*Mimic) WaitForIdle ¶
WaitForIdle causes the emulated terminal to spin, waiting the terminal output to "stabilize" (i.e. no writes are occurring)
type Option ¶
type Option func(*mimicOpt)
Option extends functionality of Mimic via functional options. see WithOutput, WithStdout, WithSize
func WithFlushTimeout ¶ added in v0.0.4
WithFlushTimeout defines the timeout for mimic's flush operation. Mimic will invoke flush only if there are outstanding operations from Mimic.Write or Mimic.WriteString.
func WithIdleDuration ¶
WithIdleDuration defines the duration required for mimic to consider the terminal idle via Mimic.WaitForIdle.
func WithIdleTimeout ¶
WithIdleTimeout defines the timeout period for mimic operations which wait for the terminal to become idle
func WithOutput ¶
WithOutput writes a copy of emulated console output to w Not compatible with WithStdout
func WithPipeFromOS ¶
func WithPipeFromOS() Option
WithPipeFromOS determines whether standard os streams should be included in the pseudo terminal
type PatternError ¶
func (PatternError) Error ¶
func (p PatternError) Error() string
type Viewer ¶
Viewer is a utility for providing a String function on a mimic value. This is intentionally separated from mimic.Mimic to allow for multiple outputs for a single mimic, and to remove any confusion about what String might refer to.
func (*Viewer) String ¶
String provides the full underlying dump of the terminal's view.
Example ¶
package main import ( "fmt" "time" "github.com/jimschubert/mimic" ) func main() { m, _ := mimic.NewMimic( mimic.WithSize(24, 80), mimic.WithIdleTimeout(300*time.Millisecond), ) text := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed vulputate odio ut enim blandit volutpat maecenas volutpat." _, _ = m.WriteString(text) _ = m.Flush() formatted := mimic.Viewer{Mimic: m, StripAnsi: true, Trim: true} fmt.Printf("%s\n", formatted.String()) }
Output: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i ncididunt ut labore et dolore magna aliqua. Sed vulputate odio ut enim blandit v olutpat maecenas volutpat.