Permalink
Browse files

Initial integration tests.

Adds helper functions to start/stop gonit, run gonit commands and
assert the existence of processes and log messages.

Change-Id: I54857ace9fef23973d2cec3aaa675b1f5611e122
  • Loading branch information...
1 parent 9b1d6e0 commit 68e7789a0a4d610c5910feb13864beb7d77b2026 @lisbakke lisbakke committed Oct 18, 2012
Showing with 474 additions and 26 deletions.
  1. +0 −5 api_test.go
  2. +1 −0 process.go
  3. +236 −21 test/helper/helper.go
  4. +96 −0 test/integration/control_test.go
  5. +79 −0 test/integration/eventmonitor_test.go
  6. +62 −0 test/integration/main_test.go
View
5 api_test.go
@@ -4,7 +4,6 @@ package gonit_test
import (
"bytes"
- "flag"
. "github.com/cloudfoundry/gonit"
"github.com/cloudfoundry/gonit/test/helper"
"io/ioutil"
@@ -22,10 +21,6 @@ var _ = Suite(&ApiSuite{})
// Hook up gocheck into the gotest runner.
func Test(t *testing.T) {
- // propagate 'go test -v' to '-gocheck.vv'
- if flag.Lookup("test.v").Value.String() == "true" {
- flag.Lookup("gocheck.vv").Value.Set("true")
- }
TestingT(t)
}
View
1 process.go
@@ -110,6 +110,7 @@ func (p *Process) Spawn(program string) (*exec.Cmd, error) {
func (p *Process) StartProcess() (int, error) {
cmd, err := p.Spawn(p.Start)
if err != nil {
+ Log.Errorf("Error starting process '%v': %v", p.Name, err.Error())
return 0, err
}
View
257 test/helper/helper.go
@@ -3,19 +3,25 @@
package helper
import (
+ "bufio"
"encoding/json"
+ "fmt"
. "github.com/cloudfoundry/gonit"
+ "github.com/cloudfoundry/gosteno"
"io"
"io/ioutil"
+ "launchpad.net/goyaml"
"log"
"net/rpc"
"net/rpc/jsonrpc"
"os"
"os/exec"
"path"
"path/filepath"
+ "strconv"
"strings"
"syscall"
+ "time"
)
type ProcessInfo struct {
@@ -36,6 +42,7 @@ type ProcessInfo struct {
}
var TestProcess, goprocess string
+var MAX_GONIT_RETRIES int = 10
func CurrentProcessInfo() *ProcessInfo {
var hasTty bool
@@ -93,25 +100,6 @@ func ReadData(data interface{}, file string) {
}
}
-// compile "test/$name/main.go" to go test's tmpdir
-// and return path to the executable
-func BuildTestProgram(name string) string {
- dir := path.Dir(os.Args[0])
- path := filepath.Join(dir, "go"+name)
- main := filepath.Join("test", name, "main.go")
-
- cmd := exec.Command("go", "build", "-o", path, main)
- cmd.Stderr = os.Stderr
- cmd.Stdout = os.Stdout
-
- err := cmd.Run()
- if err != nil {
- log.Fatal(err)
- }
-
- return path
-}
-
func TouchFile(name string, mode os.FileMode) {
dst, err := os.Create(name)
if err != nil {
@@ -204,7 +192,11 @@ func NewTestProcess(name string, flags []string, detached bool) *Process {
if goprocess == "" {
// binary used for the majority of tests
- goprocess = BuildTestProgram("process")
+ goprocess = path.Dir(os.Args[0]) + "/goprocess"
+ err := BuildBin("/test/process", goprocess)
+ if err != nil {
+ log.Fatal(err)
+ }
}
// see TempDir comment; copy goprocess where any user can execute
@@ -237,7 +229,6 @@ func NewTestProcess(name string, flags []string, detached bool) *Process {
}
start = mkcmd(args, "start")
-
return &Process{
Name: name,
Start: strings.Join(start, " "),
@@ -250,3 +241,227 @@ func NewTestProcess(name string, flags []string, detached bool) *Process {
Detached: detached,
}
}
+
+func CreateGonitCfg(numProcesses int, pname string, writePath string,
+ procPath string, includeEvents bool) error {
+ pg := &ProcessGroup{}
+ processes := map[string]*Process{}
+ for i := 0; i < numProcesses; i++ {
+ procName := pname + strconv.Itoa(i)
+ pidfile := fmt.Sprintf("%v/%v.pid", writePath, procName)
+ runCmd := fmt.Sprintf("%v -d %v -n %v -p %v", procPath, writePath, procName,
+ pidfile)
+ if includeEvents {
+ runCmd += " -MB"
+ }
+ process := &Process{
+ Name: procName,
+ Description: "Test process " + procName,
+ Start: runCmd + " start",
+ Stop: runCmd + " stop",
+ Restart: runCmd + " restart",
+ Pidfile: pidfile,
+ }
+ if includeEvents {
+ process.Actions = map[string][]string{}
+ process.Actions["alert"] = []string{"memory_over_1"}
+ process.Actions["restart"] = []string{"memory_over_6"}
+ memoryOver1 := &Event{
+ Name: "memory_over_1",
+ Description: "The memory for a process is too high",
+ Rule: "memory_used > 1mb",
+ Interval: "1s",
+ Duration: "1s",
+ }
+ memoryOver6 := &Event{
+ Name: "memory_over_6",
+ Description: "The memory for a process is too high",
+ Rule: "memory_used > 6mb",
+ Interval: "1s",
+ Duration: "1s",
+ }
+ events := map[string]*Event{}
+ events["memory_over_1"] = memoryOver1
+ events["memory_over_6"] = memoryOver6
+ pg.Events = events
+ }
+ processes[procName] = process
+ }
+ pg.Processes = processes
+ if yaml, err := goyaml.Marshal(pg); err != nil {
+ return err
+ } else {
+ gonitCfgPath := fmt.Sprintf("%v/%v-gonit.yml", writePath, pname)
+ if err := ioutil.WriteFile(gonitCfgPath, yaml, 0666); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func CreateGonitSettings(gonitPidfile string, gonitDir string, procDir string) {
+ logging := &LoggerConfig{Codec: "json"}
+ settings := &Settings{Logging: logging}
+ daemon := &Process{
+ Pidfile: gonitPidfile,
+ Dir: gonitDir,
+ Name: "gonit",
+ }
+ settings.Daemon = daemon
+ yaml, _ := goyaml.Marshal(settings)
+ err := ioutil.WriteFile(procDir+"/gonit.yml", yaml, 0666)
+ if err != nil {
+ log.Fatalf("WriteFile(%s): %v", procDir+"/gonit.yml", err)
+ }
+}
+
+// Read pid from a file.
+func ProxyReadPidFile(path string) (int, error) {
+ return ReadPidFile(path)
+}
+
+// Given the path to a direcotry to build and given an optional output path,
+// this will build the binary.
+func BuildBin(path string, outputPath string) error {
+ log.Printf("Building '%v'", path)
+ var output []byte
+ var err error
+ output, err = exec.Command("go", "build", "-o", outputPath,
+ path+"/main.go").Output()
+ if err != nil {
+ return fmt.Errorf("Error building bin '%v': %v", path, string(output))
+ }
+ return nil
+}
+
+// Given a gonit command, prints out any stderr messages.
+func printCmdStderr(gonitCmd *exec.Cmd, stderr io.ReadCloser) {
+ reader := bufio.NewReader(stderr)
+ for {
+ line, _, err := reader.ReadLine()
+ if err != nil {
+ break
+ }
+ log.Printf("Gonit stderr message: %+v", string(line))
+ }
+ gonitCmd.Process.Wait()
+}
+
+// Given a config directory, this will start gonit, output the log messages and
+// watch gonit to print out error messages.
+func StartGonit(configDir string) (*exec.Cmd, io.ReadCloser, error) {
+ cmd := exec.Command("./gonit", "-d", "10", "-c", configDir)
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Println(err)
+ }
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ log.Println(err)
+ }
+ if err := cmd.Start(); err != nil {
+ return nil, nil, fmt.Errorf("Error starting gonit: %v", err.Error())
+ }
+ log.Printf("Started gonit pid %v.", cmd.Process.Pid)
+ go printCmdStderr(cmd, stderr)
+ err = waitUntilRunning(configDir)
+ return cmd, stdout, err
+}
+
+// This kills all processes gonit is running, then stops gonit. Path is used
+// because we set custom pid file locations so the gonit client needs to know
+// where the config files are for that.
+func StopGonit(gonitCmd *exec.Cmd, path string) error {
+ pid := gonitCmd.Process.Pid
+ if !DoesProcessExist(pid) {
+ return fmt.Errorf("Gonit process died unexpectedly.")
+ }
+ if err := RunGonitCmd("stop all", path); err != nil {
+ return err
+ }
+ gonitCmd.Process.Kill()
+ log.Printf("Killed gonit pid %v.", pid)
+ return nil
+}
+
+// Waits for gonit to indicate that it is ready to take API requests.
+func waitUntilRunning(path string) error {
+ for i := 0; i < MAX_GONIT_RETRIES; i++ {
+ log.Printf("Waiting for gonit.")
+ if err := RunGonitCmd("about", path); err == nil {
+ log.Printf("Gonit is ready.")
+ return nil
+ }
+ time.Sleep(200 * time.Millisecond)
+ }
+ return fmt.Errorf("Could not connect to gonit.")
+}
+
+// Returns whether a given pid is running.
+func DoesProcessExist(pid int) bool {
+ err := syscall.Kill(pid, 0)
+ if err != nil {
+ return false
+ }
+
+ return true
+}
+
+// A helper function used by FindLogLine to set a timeout that will trigger
+// FindLogLine to return false if the log line is not found within the timeout
+// duration.
+func setTimedOut(duration time.Duration, timedout *bool) {
+ for {
+ select {
+ case <-time.After(duration):
+ *timedout = true
+ break
+ }
+ }
+}
+
+// Given an stdout pipe, a logline to find and a timeout string, this will
+// return whether the logline was printed or not.
+func FindLogLine(stdout io.ReadCloser, logline string, timeout string) bool {
+ duration, err := time.ParseDuration(timeout)
+ if err != nil {
+ log.Printf("Invalid duration '%v'", timeout)
+ return false
+ }
+ reader := bufio.NewReader(stdout)
+ timedout := false
+ go setTimedOut(duration, &timedout)
+ prettifier := steno.NewJsonPrettifier(steno.EXCLUDE_NONE)
+ for {
+ line, _, err := reader.ReadLine()
+ if err != nil {
+ break
+ }
+ // TODO(lisbakke): It is possible we won't get here if ReadLine hangs.
+ if timedout {
+ log.Printf("Finding log line '%v' timed out.", logline)
+ return false
+ }
+ record, err := prettifier.DecodeJsonLogEntry(string(line))
+ if err != nil {
+ // If we have an error, it's likely because the configmanager logs some
+ // stuff before the steno log is told to output JSON, so we get some
+ // non-JSON messages that can't be parsed.
+ continue
+ }
+ if strings.Contains(record.Message, logline) {
+ return true
+ }
+ }
+ return false
+}
+
+// Given a gonit command string such as "start all", this will run the command.
+// Path is used because we set custom pid file locations so the gonit client
+// needs to know where the config files are for that.
+func RunGonitCmd(command string, path string) error {
+ command = "-c " + path + " " + command
+ log.Printf("Running command: './gonit %v'", command)
+ cmd := exec.Command("./gonit", strings.Fields(command)...)
+ return cmd.Run()
+}
View
96 test/integration/control_test.go
@@ -0,0 +1,96 @@
+// Copyright (c) 2012 VMware, Inc.
+
+package gonit_integration
+
+import (
+ "github.com/cloudfoundry/gonit/test/helper"
+ . "launchpad.net/gocheck"
+ "os"
+ "os/exec"
+)
+
+type ControlIntSuite struct {
+ gonitCmd *exec.Cmd
+ ctrlCfgDir string
+}
+
+var _ = Suite(&ControlIntSuite{})
+
+var controlPidFile1 string
+var controlPidFile2 string
+
+func (s *ControlIntSuite) SetUpTest(c *C) {
+ s.ctrlCfgDir = integrationDir + "/tmp/process_tmp"
+ if err := os.MkdirAll(s.ctrlCfgDir, 0755); err != nil {
+ c.Errorf(err.Error())
+ }
+ controlPidFile1 = s.ctrlCfgDir + "/control0.pid"
+ controlPidFile2 = s.ctrlCfgDir + "/control1.pid"
+ err := helper.CreateGonitCfg(2, "control", "./process_tmp", "./goprocess",
+ false)
+ if err != nil {
+ c.Errorf(err.Error())
+ }
+ helper.CreateGonitSettings("./gonit.pid", "./", "./process_tmp")
+ s.gonitCmd, _, err = helper.StartGonit(s.ctrlCfgDir)
+ if err != nil {
+ c.Errorf(err.Error())
+ }
+}
+
+func (s *ControlIntSuite) TearDownTest(c *C) {
+ if err := helper.StopGonit(s.gonitCmd, s.ctrlCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+ if err := os.RemoveAll(s.ctrlCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+}
+
+func (s *ControlIntSuite) TestStartStop(c *C) {
+ if err := helper.RunGonitCmd("start all", s.ctrlCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+
+ pid1, err := helper.ProxyReadPidFile(controlPidFile1)
+ c.Check(err, IsNil)
+ pid2, err := helper.ProxyReadPidFile(controlPidFile2)
+ c.Check(err, IsNil)
+
+ c.Check(helper.DoesProcessExist(pid1), Equals, true)
+ c.Check(helper.DoesProcessExist(pid2), Equals, true)
+ if err := helper.RunGonitCmd("stop all", s.ctrlCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+ c.Check(helper.DoesProcessExist(pid1), Equals, false)
+ c.Check(helper.DoesProcessExist(pid2), Equals, false)
+}
+
+func (s *ControlIntSuite) TestRestart(c *C) {
+ if err := helper.RunGonitCmd("start all", s.ctrlCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+
+ firstPid1, err := helper.ProxyReadPidFile(controlPidFile1)
+ c.Check(err, IsNil)
+ firstPid2, err := helper.ProxyReadPidFile(controlPidFile2)
+ c.Check(err, IsNil)
+
+ c.Check(helper.DoesProcessExist(firstPid1), Equals, true)
+ c.Check(helper.DoesProcessExist(firstPid2), Equals, true)
+ if err := helper.RunGonitCmd("restart all", s.ctrlCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+
+ secondPid1, err := helper.ProxyReadPidFile(controlPidFile1)
+ c.Check(err, IsNil)
+ secondPid2, err := helper.ProxyReadPidFile(controlPidFile2)
+ c.Check(err, IsNil)
+
+ c.Check(helper.DoesProcessExist(secondPid1), Equals, true)
+ c.Check(helper.DoesProcessExist(secondPid2), Equals, true)
+ c.Check(helper.DoesProcessExist(firstPid1), Equals, false)
+ c.Check(helper.DoesProcessExist(firstPid2), Equals, false)
+ c.Check(firstPid1, Not(Equals), secondPid1)
+ c.Check(firstPid2, Not(Equals), secondPid2)
+}
View
79 test/integration/eventmonitor_test.go
@@ -0,0 +1,79 @@
+// Copyright (c) 2012 VMware, Inc.
+
+package gonit_integration
+
+import (
+ "github.com/cloudfoundry/gonit/test/helper"
+ "io"
+ . "launchpad.net/gocheck"
+ "os"
+ "os/exec"
+)
+
+type EventIntSuite struct {
+ gonitCmd *exec.Cmd
+ stdout io.ReadCloser
+ eventCfgDir string
+}
+
+var _ = Suite(&EventIntSuite{})
+
+var balloonPidFile string
+
+func (s *EventIntSuite) SetUpTest(c *C) {
+ s.eventCfgDir = integrationDir + "/tmp/process_tmp"
+ if err := os.MkdirAll(s.eventCfgDir, 0755); err != nil {
+ c.Errorf(err.Error())
+ }
+ balloonPidFile = s.eventCfgDir + "/balloonmem0.pid"
+ err := helper.CreateGonitCfg(1, "balloonmem", "./process_tmp", "./goprocess",
+ true)
+ if err != nil {
+ c.Errorf(err.Error())
+ }
+ helper.CreateGonitSettings("./gonit.pid", "./", "./process_tmp")
+ s.gonitCmd, s.stdout, err = helper.StartGonit(s.eventCfgDir)
+ if err != nil {
+ c.Errorf(err.Error())
+ }
+}
+
+func (s *EventIntSuite) TearDownTest(c *C) {
+ if err := helper.StopGonit(s.gonitCmd, s.eventCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+ if err := os.RemoveAll(s.eventCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+}
+
+func (s *EventIntSuite) TestAlertRule(c *C) {
+ if err := helper.RunGonitCmd("start all", s.eventCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+ c.Check(true, Equals, helper.FindLogLine(s.stdout,
+ "'balloonmem0' triggered 'memory_used > 1mb' for '1s'", "5s"))
+}
+
+func (s *EventIntSuite) TestRestartRule(c *C) {
+ if err := helper.RunGonitCmd("start all", s.eventCfgDir); err != nil {
+ c.Errorf(err.Error())
+ }
+
+ balloonPid, err := helper.ProxyReadPidFile(balloonPidFile)
+ c.Check(err, IsNil)
+ c.Check(helper.DoesProcessExist(balloonPid), Equals, true)
+ pid1, _ := helper.ProxyReadPidFile(balloonPidFile)
+ c.Check(true, Equals,
+ helper.FindLogLine(s.stdout, "Executing 'restart'", "20s"))
+ c.Check(true, Equals,
+ helper.FindLogLine(s.stdout, "process \"balloonmem0\" started", "10s"))
+ balloonPid, err = helper.ProxyReadPidFile(balloonPidFile)
+ c.Check(err, IsNil)
+ c.Check(helper.DoesProcessExist(balloonPid), Equals, true)
+ pid2, err := helper.ProxyReadPidFile(balloonPidFile)
+ if err != nil {
+ c.Errorf(err.Error())
+ }
+ c.Check(pid1, Not(Equals), pid2)
+}
View
62 test/integration/main_test.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2012 VMware, Inc.
+
+package gonit_integration
+
+import (
+ "flag"
+ "github.com/cloudfoundry/gonit/test/helper"
+ . "launchpad.net/gocheck"
+ "log"
+ "os"
+ "testing"
+)
+
+var (
+ integrationDir string
+ gonitMainDir string
+ integrationTmpDir string
+)
+
+func setup() {
+ flag.Parse()
+
+ if intDir, err := os.Getwd(); err != nil {
+ log.Fatalf("Error getting pwd: %v", err.Error())
+ } else {
+ integrationDir = intDir
+ }
+
+ integrationTmpDir := integrationDir + "/tmp"
+ gonitMainDir = integrationDir + "/../../gonit"
+
+ if err := os.MkdirAll(integrationTmpDir, 0755); err != nil {
+ log.Fatalf(err.Error())
+ }
+
+ err := helper.BuildBin(integrationDir+"/../process",
+ integrationTmpDir+"/goprocess")
+ if err != nil {
+ log.Fatalf(err.Error())
+ }
+
+ err = helper.BuildBin(gonitMainDir, integrationTmpDir+"/gonit")
+ if err != nil {
+ log.Fatalf(err.Error())
+ }
+
+ if err := os.Chdir(integrationTmpDir); err != nil {
+ log.Fatalf("Error changing dir: %v", err.Error())
+ }
+}
+
+func cleanup() {
+ if err := os.RemoveAll(integrationDir + "/tmp"); err != nil {
+ log.Printf("Error deleting '%v': '%v'", integrationTmpDir, err.Error())
+ }
+}
+
+func Test(t *testing.T) {
+ setup()
+ TestingT(t)
+ cleanup()
+}

0 comments on commit 68e7789

Please sign in to comment.