Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ginkgomon_v2 to support Ginkgo v2+ #36

Merged
merged 1 commit into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions ginkgomon_v2/ginkgomon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
Ginkgomon_v2 provides ginkgo test helpers that are compatible with Ginkgo v2+
*/
package ginkgomon_v2

import (
"fmt"
"io"
"os"
"os/exec"
"time"

"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/gexec"
)

// Config defines a ginkgomon Runner.
type Config struct {
Command *exec.Cmd // process to be executed
Name string // prefixes all output lines
AnsiColorCode string // colors the output
StartCheck string // text to match to indicate sucessful start.
StartCheckTimeout time.Duration // how long to wait to see StartCheck
Cleanup func() // invoked once the process exits
}

/*
The ginkgomon Runner invokes a new process using gomega's gexec package.

If a start check is defined, the runner will wait until it sees the start check
before declaring ready.

Runner implements gexec.Exiter and gbytes.BufferProvider, so you can test exit
codes and process output using the appropriate gomega matchers:
http://onsi.github.io/gomega/#gexec-testing-external-processes
*/
type Runner struct {
Command *exec.Cmd
Name string
AnsiColorCode string
StartCheck string
StartCheckTimeout time.Duration
Cleanup func()
session *gexec.Session
sessionReady chan struct{}
}

// New creates a ginkgomon Runner from a config object. Runners must be created
// with New to properly initialize their internal state.
func New(config Config) *Runner {
return &Runner{
Name: config.Name,
Command: config.Command,
AnsiColorCode: config.AnsiColorCode,
StartCheck: config.StartCheck,
StartCheckTimeout: config.StartCheckTimeout,
Cleanup: config.Cleanup,
sessionReady: make(chan struct{}),
}
}

// ExitCode returns the exit code of the process, or -1 if the process has not
// exited. It can be used with the gexec.Exit matcher.
func (r *Runner) ExitCode() int {
if r.sessionReady == nil {
ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name))
}
<-r.sessionReady
return r.session.ExitCode()
}

// Buffer returns a gbytes.Buffer, for use with the gbytes.Say matcher.
func (r *Runner) Buffer() *gbytes.Buffer {
if r.sessionReady == nil {
ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name))
}
<-r.sessionReady
return r.session.Buffer()
}

// Err returns the gbytes.Buffer associated with the stderr stream.
// For use with the gbytes.Say matcher.
func (r *Runner) Err() *gbytes.Buffer {
if r.sessionReady == nil {
ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name))
}
<-r.sessionReady
return r.session.Err
}

func (r *Runner) Run(sigChan <-chan os.Signal, ready chan<- struct{}) error {
defer ginkgo.GinkgoRecover()

allOutput := gbytes.NewBuffer()

debugWriter := gexec.NewPrefixedWriter(
fmt.Sprintf("\x1b[32m[d]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name),
ginkgo.GinkgoWriter,
)

session, err := gexec.Start(
r.Command,
gexec.NewPrefixedWriter(
fmt.Sprintf("\x1b[32m[o]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name),
io.MultiWriter(allOutput, ginkgo.GinkgoWriter),
),
gexec.NewPrefixedWriter(
fmt.Sprintf("\x1b[91m[e]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name),
io.MultiWriter(allOutput, ginkgo.GinkgoWriter),
),
)

Ω(err).ShouldNot(HaveOccurred(), fmt.Sprintf("%s failed to start with err: %s", r.Name, err))

fmt.Fprintf(debugWriter, "spawned %s (pid: %d)\n", r.Command.Path, r.Command.Process.Pid)

r.session = session
if r.sessionReady != nil {
close(r.sessionReady)
}

startCheckDuration := r.StartCheckTimeout
if startCheckDuration == 0 {
startCheckDuration = 5 * time.Second
}

var startCheckTimeout <-chan time.Time
if r.StartCheck != "" {
startCheckTimeout = time.After(startCheckDuration)
}

detectStartCheck := allOutput.Detect(r.StartCheck)

for {
select {
case <-detectStartCheck: // works even with empty string
allOutput.CancelDetects()
startCheckTimeout = nil
detectStartCheck = nil
close(ready)

case <-startCheckTimeout:
// clean up hanging process
session.Kill().Wait()

// fail to start
return fmt.Errorf(
"did not see %s in command's output within %s. full output:\n\n%s",
r.StartCheck,
startCheckDuration,
string(allOutput.Contents()),
)

case signal := <-sigChan:
session.Signal(signal)

case <-session.Exited:
if r.Cleanup != nil {
r.Cleanup()
}

if session.ExitCode() == 0 {
return nil
}

return fmt.Errorf("exit status %d", session.ExitCode())
}
}
}
36 changes: 36 additions & 0 deletions ginkgomon_v2/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ginkgomon_v2

import (
"fmt"
"os"

"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/tedsuo/ifrit"
)

func Invoke(runner ifrit.Runner) ifrit.Process {
process := ifrit.Background(runner)

select {
case <-process.Ready():
case err := <-process.Wait():
ginkgo.Fail(fmt.Sprintf("process failed to start: %s", err), 1)
}

return process
}

func Interrupt(process ifrit.Process, intervals ...interface{}) {
if process != nil {
process.Signal(os.Interrupt)
EventuallyWithOffset(1, process.Wait(), intervals...).Should(Receive(), "interrupted ginkgomon process failed to exit in time")
}
}

func Kill(process ifrit.Process, intervals ...interface{}) {
if process != nil {
process.Signal(os.Kill)
EventuallyWithOffset(1, process.Wait(), intervals...).Should(Receive(), "killed ginkgomon process failed to exit in time")
}
}