Skip to content
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
43 changes: 41 additions & 2 deletions tests/cmd/git/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,29 @@ package git
import (
"fmt"
"net/http"
"os"
"strconv"
"time"

"github.com/deis/workflow-e2e/tests/cmd"
"github.com/deis/workflow-e2e/tests/model"
"github.com/deis/workflow-e2e/tests/settings"

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

// The functions in this file implement SUCCESS CASES for commonly used `git` commands.
// This allows each of these to be re-used easily in multiple contexts.

const (
pushCommandLineString = "GIT_SSH=%s GIT_KEY=%s git push deis master"
)

// Push executes a `git push deis master` from the current directory using the provided key.
func Push(user model.User, keyPath string, app model.App, banner string) {
sess, err := cmd.Start("GIT_SSH=%s GIT_KEY=%s git push deis master", &user, settings.GitSSH, keyPath)
Expect(err).NotTo(HaveOccurred())
sess := doGitPush(user, keyPath)
// sess.Wait(settings.MaxEventuallyTimeout)
// output := string(sess.Out.Contents())
// Expect(output).To(MatchRegexp(`Done, %s:v\d deployed to Deis`, app.Name))
Expand All @@ -33,3 +39,36 @@ func Push(user model.User, keyPath string, app model.App, banner string) {
curlCmd = model.Cmd{CommandLineString: fmt.Sprintf(`curl -sL "%s"`, app.URL)}
Eventually(cmd.Retry(curlCmd, banner, cmdRetryTimeout)).Should(BeTrue())
}

// PushWithInterrupt executes a `git push deis master` from the current
// directory using the provided key, but then halts the progress via SIGINT.
func PushWithInterrupt(user model.User, keyPath string) {
sess := doGitPush(user, keyPath)
Eventually(sess.Err).Should(Say("Starting build... but first, coffee!"))

sess = sess.Interrupt()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bummer b/c it could introduce a timing dependency (the git push could complete before this executes)

I know there's no reasonable fix right now, so this is me just recording the thought. consider it the equivalent of the tiniest nit for now!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arschles Yes, good point. Although gomega's Eventually polls the sess provided and returns immediately once the supplied string is found (or, if never found, hits the default timeout), it seems anytime 'Eventually' is used on a sess, there's no way to actually interrupt/kill/stop that session. I just noticed when I turned on verbose output, the original git push deis master cmd is still ongoing in the background.

Alternatively, if we pull out the Eventually check, the cmd is indeed terminated... but then we'd end up needing an even uglier time.Sleep(duration) to wait for the cmd to progress a bit before interrupting. Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what's here is fine for now


newSess := doGitPush(user, keyPath)
Eventually(newSess.Err).ShouldNot(Say("exec request failed on channel 0"))
Eventually(newSess.Err).Should(Say("fatal: remote error: Another git push is ongoing"))
Eventually(newSess, settings.DefaultEventuallyTimeout).Should(Exit(128))
}

// PushUntilResult executes a `git push deis master` from the current
// directory using the provided key, until the command result satisfies
// expectedCmdResult of type model.CmdResult, failing if
// settings.DefaultEventuallyTimeout is reached first.
func PushUntilResult(user model.User, keyPath string, expectedCmdResult model.CmdResult) {
envVars := append(os.Environ(), fmt.Sprintf("DEIS_PROFILE=%s", user.Username))
pushCmd := model.Cmd{Env: envVars, CommandLineString: fmt.Sprintf(
pushCommandLineString, settings.GitSSH, keyPath)}

Eventually(cmd.RetryUntilResult(pushCmd, expectedCmdResult, 5*time.Second,
settings.MaxEventuallyTimeout)).Should(BeTrue())
}

func doGitPush(user model.User, keyPath string) *Session {
sess, err := cmd.Start(pushCommandLineString, &user, settings.GitSSH, keyPath)
Expect(err).NotTo(HaveOccurred())
return sess
}
36 changes: 36 additions & 0 deletions tests/cmd/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func StartCmd(command model.Cmd) (*gexec.Session, error) {
// supplied <timeout> until the <cmd> result contains the <expectedResult>
// An example use of this utility would be curl-ing a url and waiting
// until the response code matches the expected response.
// TODO: https://github.com/deis/workflow-e2e/issues/240
func Retry(command model.Cmd, expectedResult string, timeout int) bool {
var result string
fmt.Fprintf(ginkgo.GinkgoWriter, "Waiting up to %d seconds for `%s` to return %s...\n", timeout, command.CommandLineString, expectedResult)
Expand All @@ -81,3 +82,38 @@ func Retry(command model.Cmd, expectedResult string, timeout int) bool {
fmt.Fprintf(ginkgo.GinkgoWriter, "FAIL: '%s' does not match expected result of '%s'\n", result, expectedResult)
return false
}

// RetryUntilResult runs the provided cmd repeatedly, once every period,
// up to the supplied timeout until the cmd result matches the supplied
// expectedCmdResult
func RetryUntilResult(command model.Cmd, expectedCmdResult model.CmdResult, period, timeout time.Duration) bool {
var actualCmdResult model.CmdResult

fmt.Fprintf(ginkgo.GinkgoWriter,
"Waiting up to %d seconds for `%s` to return expected cmdResult %s...\n",
int(timeout.Seconds()), command.CommandLineString, expectedCmdResult.String())

tck := time.NewTicker(period)
tmr := time.NewTimer(timeout)
defer tck.Stop()
defer tmr.Stop()
for {
select {
case <-tck.C:
sess, err := StartCmd(command)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
sessWait := sess.Wait()
actualCmdResult = model.CmdResult{
Out: sessWait.Out.Contents(),
Err: sessWait.Err.Contents(),
ExitCode: sessWait.ExitCode(),
}
if actualCmdResult.Satisfies(expectedCmdResult) {
return true
}
case <-tmr.C:
fmt.Fprintf(ginkgo.GinkgoWriter, "FAIL: Actual cmdResult '%v' does not match expected cmdResult '%v'\n", actualCmdResult, expectedCmdResult)
return false
}
}
}
11 changes: 11 additions & 0 deletions tests/git_push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ var _ = Describe("git push deis master", func() {
git.Push(user, keyPath, app, "Powered by Deis")
})

Specify("that user can interrupt the deploy of the app and recover", func() {
git.PushWithInterrupt(user, keyPath)

git.PushUntilResult(user, keyPath,
model.CmdResult{
Out: nil,
Err: []byte("Everything up-to-date"),
ExitCode: 0,
})
})

})

})
Expand Down
29 changes: 29 additions & 0 deletions tests/model/model.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package model

import (
"bytes"
"fmt"
"math/rand"
"path"
Expand Down Expand Up @@ -72,3 +73,31 @@ func getDir() string {
_, filename, _, _ := runtime.Caller(1)
return path.Dir(filename)
}

// CmdResult represents a generic command result, with expected Out, Err and
// ExitCode
type CmdResult struct {
Out []byte
Err []byte
ExitCode int
}

// Satisfies returns whether or not the original CmdResult, ocd, meets all of
// the expectations contained in the expeced CmdResult, ecd
func (ocd CmdResult) Satisfies(ecd CmdResult) bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this receiver name describes my coding style perfectly

if !bytes.Contains(ocd.Out, ecd.Out) {
return false
}
if !bytes.Contains(ocd.Err, ecd.Err) {
return false
}
if ocd.ExitCode != ecd.ExitCode {
return false
}
return true
}

// String returns the CmdResult in printable form
func (ocd CmdResult) String() string {
return fmt.Sprintf("[Out: '%s', Err: '%s', ExitCode: '%d']", ocd.Out, ocd.Err, ocd.ExitCode)
}