Documentation
¶
Overview ¶
Package mimic provides a utility for interacting with console or terminal based applications.
The Mimic type internally constructs two pseudo-terminals: one wrapping go-expect, and another constructor from creack/pty. This allows for either stream-based or view-based inspection of strings/patterns.
Stream-based inspections are useful for interactive flows where you need to wait for and respond to prompts in real-time. View-based inspections are better for validating final terminal state after all output has been rendered.
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 (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")
// ExpectString accepts multiple strings to match
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 goroutine 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. The default SuiteOptions apply a maximum runtime for the suite, and you can override SuiteOptions on your embedded type to change or extend these defaults. Here's a 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!")
// Stream-based: raw pty sees all characters, including wrapped ones
assert.NoError(m.T(), console.ExpectString(full), "Stream should contain full %d character string", fullWriteWidth)
// View-based: formatted terminal view shows wrapping behavior
assert.False(m.T(), console.ContainsString(full), "View should not contain unwrapped %d character string", fullWriteWidth)
assert.True(m.T(), console.ContainsString(strings.Repeat(character, terminalWidth)+"\n"+strings.Repeat(character, wrapLength)), "View should show text wrapped at %d columns", terminalWidth)
}
func TestMimicOperationsSuite(t *testing.T) {
test := new(MyTests)
test.suiteRuntimeDuration = 30 * time.Second
test.Init(WithMaxRuntime(test.suiteRuntimeDuration))
suite.Run(t, test)
}
Errors and pattern matching ¶
In addition to the bool-returning helpers ContainsString and ContainsPattern, Mimic also provides ContainsStringE and ContainsPatternE which return a bool and error. ContainsStringE reports underlying flush/console errors, while ContainsPatternE will return a PatternError when the contents do not satisfy all requested patterns. PatternError exposes both the failed patterns and the terminal contents at the time of evaluation to aid in debugging.
Index ¶
- Constants
- type Experimental
- type Mimic
- func (m *Mimic) Close() (err error)
- func (m *Mimic) ContainsPattern(pattern ...string) bool
- func (m *Mimic) ContainsPatternE(pattern ...string) (bool, error)
- func (m *Mimic) ContainsString(str ...string) bool
- func (m *Mimic) ContainsStringE(str ...string) (bool, error)
- 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) ContainsPatternE ¶ added in v0.1.0
ContainsPatternE 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. Returns a PatternError when contents do not satisfy all requested patterns.
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) ContainsStringE ¶ added in v0.1.0
ContainsStringE 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. Returns an error if the underlying console cannot be flushed.
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. Will panic if invoked more than once.
func WithPipeFromOS ¶
func WithPipeFromOS() Option
WithPipeFromOS determines whether standard os streams should be included in the pseudo terminal
type PatternError ¶
PatternError describes a failure to match one or more patterns against terminal contents returned from a Mimic.
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.