254 lines
6.8 KiB
Go
254 lines
6.8 KiB
Go
//go:build !windows
|
|
|
|
package iostreams
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Netflix/go-expect"
|
|
"github.com/creack/pty"
|
|
"github.com/hinshun/vt10x"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestStartProgressIndicatorWithLabel(t *testing.T) {
|
|
osOut := os.Stdout
|
|
defer func() { os.Stdout = osOut }()
|
|
// Why do we need a channel in these tests to implement a timeout instead of
|
|
// relying on expect's timeout?
|
|
//
|
|
// Well, expect's timeout is based on the maximum time of a single read
|
|
// from the console. This works in cases like prompting where we block
|
|
// waiting for input because the console is not ready to be read.
|
|
// But in this case, we are not blocking waiting for input and stdout
|
|
// can be constantly read. This means the timeout will never be reached
|
|
// in the event of a expectation failure.
|
|
// To fix this, we need to implement our own timeout that is based
|
|
// specifically on the total time spent reading the console and waiting
|
|
// for the target string instead of the max time for a single read
|
|
// from the console.
|
|
t.Run("progress indicator respects GH_SPINNER_DISABLED is true", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
io := newTestIOStreams(t, console, true)
|
|
|
|
done := make(chan error)
|
|
|
|
go func() {
|
|
_, err := console.ExpectString("Working...")
|
|
done <- err
|
|
}()
|
|
|
|
io.StartProgressIndicatorWithLabel("")
|
|
defer io.StopProgressIndicator()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Test timed out waiting for progress indicator")
|
|
}
|
|
})
|
|
|
|
t.Run("progress indicator respects GH_SPINNER_DISABLED is false", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
io := newTestIOStreams(t, console, false)
|
|
|
|
done := make(chan error)
|
|
|
|
go func() {
|
|
_, err := console.ExpectString("⣾")
|
|
done <- err
|
|
}()
|
|
|
|
io.StartProgressIndicatorWithLabel("")
|
|
defer io.StopProgressIndicator()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Test timed out waiting for progress indicator")
|
|
}
|
|
})
|
|
|
|
t.Run("progress indicator with GH_SPINNER_DISABLED shows label", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
io := newTestIOStreams(t, console, true)
|
|
progressIndicatorLabel := "downloading happiness"
|
|
|
|
done := make(chan error)
|
|
|
|
go func() {
|
|
_, err := console.ExpectString(progressIndicatorLabel + "...")
|
|
done <- err
|
|
}()
|
|
|
|
io.StartProgressIndicatorWithLabel(progressIndicatorLabel)
|
|
defer io.StopProgressIndicator()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Test timed out waiting for progress indicator")
|
|
}
|
|
})
|
|
|
|
t.Run("progress indicator shows label and spinner", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
io := newTestIOStreams(t, console, false)
|
|
progressIndicatorLabel := "downloading happiness"
|
|
|
|
done := make(chan error)
|
|
|
|
go func() {
|
|
_, err := console.ExpectString(progressIndicatorLabel)
|
|
require.NoError(t, err)
|
|
_, err = console.ExpectString("⣾")
|
|
done <- err
|
|
}()
|
|
|
|
io.StartProgressIndicatorWithLabel(progressIndicatorLabel)
|
|
defer io.StopProgressIndicator()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Test timed out waiting for progress indicator")
|
|
}
|
|
})
|
|
|
|
t.Run("multiple calls to start progress indicator with GH_SPINNER_DISABLED prints additional labels", func(t *testing.T) {
|
|
console := newTestVirtualTerminal(t)
|
|
io := newTestIOStreams(t, console, true)
|
|
progressIndicatorLabel1 := "downloading happiness"
|
|
progressIndicatorLabel2 := "downloading sadness"
|
|
done := make(chan error)
|
|
go func() {
|
|
_, err := console.ExpectString(progressIndicatorLabel1 + "...")
|
|
require.NoError(t, err)
|
|
_, err = console.ExpectString(progressIndicatorLabel2 + "...")
|
|
done <- err
|
|
}()
|
|
io.StartProgressIndicatorWithLabel(progressIndicatorLabel1)
|
|
defer io.StopProgressIndicator()
|
|
io.StartProgressIndicatorWithLabel(progressIndicatorLabel2)
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Test timed out waiting for progress indicator")
|
|
}
|
|
})
|
|
}
|
|
|
|
func newTestVirtualTerminal(t *testing.T) *expect.Console {
|
|
t.Helper()
|
|
|
|
// Create a PTY and hook up a virtual terminal emulator
|
|
ptm, pts, err := pty.Open()
|
|
require.NoError(t, err)
|
|
|
|
term := vt10x.New(vt10x.WithWriter(pts))
|
|
|
|
// Create a console via Expect that allows scripting against the terminal
|
|
consoleOpts := []expect.ConsoleOpt{
|
|
expect.WithStdin(ptm),
|
|
expect.WithStdout(term),
|
|
expect.WithCloser(ptm, pts),
|
|
failOnExpectError(t),
|
|
failOnSendError(t),
|
|
expect.WithDefaultTimeout(time.Second),
|
|
}
|
|
|
|
console, err := expect.NewConsole(consoleOpts...)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { testCloser(t, console) })
|
|
|
|
return console
|
|
}
|
|
|
|
func newTestIOStreams(t *testing.T, console *expect.Console, spinnerDisabled bool) *IOStreams {
|
|
t.Helper()
|
|
|
|
in := console.Tty()
|
|
out := console.Tty()
|
|
errOut := console.Tty()
|
|
|
|
// Because the briandowns/spinner checks os.Stdout directly,
|
|
// we need this hack to trick it into allowing the spinner to print...
|
|
os.Stdout = out
|
|
|
|
io := &IOStreams{
|
|
In: in,
|
|
Out: out,
|
|
ErrOut: errOut,
|
|
term: fakeTerm{},
|
|
}
|
|
io.progressIndicatorEnabled = true
|
|
io.SetSpinnerDisabled(spinnerDisabled)
|
|
return io
|
|
}
|
|
|
|
// failOnExpectError adds an observer that will fail the test in a standardised way
|
|
// if any expectation on the command output fails, without requiring an explicit
|
|
// assertion.
|
|
//
|
|
// Use WithRelaxedIO to disable this behaviour.
|
|
func failOnExpectError(t *testing.T) expect.ConsoleOpt {
|
|
t.Helper()
|
|
return expect.WithExpectObserver(
|
|
func(matchers []expect.Matcher, buf string, err error) {
|
|
t.Helper()
|
|
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
if len(matchers) == 0 {
|
|
t.Fatalf("Error occurred while matching %q: %s\n", buf, err)
|
|
}
|
|
|
|
var criteria []string
|
|
for _, matcher := range matchers {
|
|
criteria = append(criteria, fmt.Sprintf("%q", matcher.Criteria()))
|
|
}
|
|
t.Fatalf("Failed to find [%s] in %q: %s\n", strings.Join(criteria, ", "), buf, err)
|
|
},
|
|
)
|
|
}
|
|
|
|
// failOnSendError adds an observer that will fail the test in a standardised way
|
|
// if any sending of input fails, without requiring an explicit assertion.
|
|
//
|
|
// Use WithRelaxedIO to disable this behaviour.
|
|
func failOnSendError(t *testing.T) expect.ConsoleOpt {
|
|
t.Helper()
|
|
return expect.WithSendObserver(
|
|
func(msg string, n int, err error) {
|
|
t.Helper()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to send %q: %s\n", msg, err)
|
|
}
|
|
if len(msg) != n {
|
|
t.Fatalf("Only sent %d of %d bytes for %q\n", n, len(msg), msg)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// testCloser is a helper to fail the test if a Closer fails to close.
|
|
func testCloser(t *testing.T, closer io.Closer) {
|
|
t.Helper()
|
|
if err := closer.Close(); err != nil {
|
|
t.Errorf("Close failed: %s", err)
|
|
}
|
|
}
|