launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/worker/uniter/debug/server_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package debug 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "regexp" 13 "time" 14 15 "launchpad.net/errgo/errors" 16 gc "launchpad.net/gocheck" 17 18 jc "launchpad.net/juju-core/testing/checkers" 19 "launchpad.net/juju-core/testing/testbase" 20 ) 21 22 type DebugHooksServerSuite struct { 23 testbase.LoggingSuite 24 ctx *HooksContext 25 fakebin string 26 tmpdir string 27 } 28 29 var _ = gc.Suite(&DebugHooksServerSuite{}) 30 31 // echocommand outputs its name and arguments to stdout for verification, 32 // and exits with the value of $EXIT_CODE 33 var echocommand = `#!/bin/bash --norc 34 echo $(basename $0) $@ 35 exit $EXIT_CODE 36 ` 37 38 var fakecommands = []string{"tmux"} 39 40 func (s *DebugHooksServerSuite) SetUpTest(c *gc.C) { 41 s.fakebin = c.MkDir() 42 s.tmpdir = c.MkDir() 43 s.PatchEnvironment("PATH", s.fakebin+":"+os.Getenv("PATH")) 44 s.PatchEnvironment("TMPDIR", s.tmpdir) 45 s.PatchEnvironment("TEST_RESULT", "") 46 for _, name := range fakecommands { 47 err := ioutil.WriteFile(filepath.Join(s.fakebin, name), []byte(echocommand), 0777) 48 c.Assert(err, gc.IsNil) 49 } 50 s.ctx = NewHooksContext("foo/8") 51 s.ctx.FlockDir = s.tmpdir 52 s.PatchEnvironment("JUJU_UNIT_NAME", s.ctx.Unit) 53 } 54 55 func (s *DebugHooksServerSuite) TestFindSession(c *gc.C) { 56 // Test "tmux has-session" failure. The error 57 // message is the output of tmux has-session. 58 os.Setenv("EXIT_CODE", "1") 59 session, err := s.ctx.FindSession() 60 c.Assert(session, gc.IsNil) 61 c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta("tmux has-session -t "+s.ctx.Unit+"\n")) 62 os.Setenv("EXIT_CODE", "") 63 64 // tmux session exists, but missing debug-hooks file: error. 65 session, err = s.ctx.FindSession() 66 c.Assert(session, gc.IsNil) 67 c.Assert(err, gc.NotNil) 68 c.Assert(errors.Cause(err), jc.Satisfies, os.IsNotExist) 69 70 // Hooks file is present, empty. 71 err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 72 c.Assert(err, gc.IsNil) 73 session, err = s.ctx.FindSession() 74 c.Assert(session, gc.NotNil) 75 c.Assert(err, gc.IsNil) 76 // If session.hooks is empty, it'll match anything. 77 c.Assert(session.MatchHook(""), jc.IsTrue) 78 c.Assert(session.MatchHook("something"), jc.IsTrue) 79 80 // Hooks file is present, non-empty 81 err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte(`hooks: [foo, bar, baz]`), 0777) 82 c.Assert(err, gc.IsNil) 83 session, err = s.ctx.FindSession() 84 c.Assert(session, gc.NotNil) 85 c.Assert(err, gc.IsNil) 86 // session should only match "foo", "bar" or "baz". 87 c.Assert(session.MatchHook(""), jc.IsFalse) 88 c.Assert(session.MatchHook("something"), jc.IsFalse) 89 c.Assert(session.MatchHook("foo"), jc.IsTrue) 90 c.Assert(session.MatchHook("bar"), jc.IsTrue) 91 c.Assert(session.MatchHook("baz"), jc.IsTrue) 92 c.Assert(session.MatchHook("foo bar baz"), jc.IsFalse) 93 } 94 95 func (s *DebugHooksServerSuite) TestRunHookExceptional(c *gc.C) { 96 err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 97 c.Assert(err, gc.IsNil) 98 session, err := s.ctx.FindSession() 99 c.Assert(session, gc.NotNil) 100 c.Assert(err, gc.IsNil) 101 102 // Run the hook in debug mode with no exit flock held. 103 // The exit flock will be acquired immediately, and the 104 // debug-hooks server process killed. 105 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 106 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 107 108 // Run the hook in debug mode, simulating the holding 109 // of the exit flock. This simulates the client process 110 // starting but not cleanly exiting (normally the .pid 111 // file is updated, and the server waits on the client 112 // process' death). 113 ch := make(chan bool) 114 var clientExited bool 115 s.PatchValue(&waitClientExit, func(*ServerSession) { 116 clientExited = <-ch 117 }) 118 go func() { ch <- true }() 119 err = session.RunHook("myhook", s.tmpdir, os.Environ()) 120 c.Assert(clientExited, jc.IsTrue) 121 c.Assert(err, gc.ErrorMatches, "signal: [kK]illed") 122 } 123 124 func (s *DebugHooksServerSuite) TestRunHook(c *gc.C) { 125 err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777) 126 c.Assert(err, gc.IsNil) 127 session, err := s.ctx.FindSession() 128 c.Assert(session, gc.NotNil) 129 c.Assert(err, gc.IsNil) 130 131 const hookName = "myhook" 132 133 // Run the hook in debug mode with the exit flock held, 134 // and also create the .pid file. We'll populate it with 135 // an invalid PID; this will cause the server process to 136 // exit cleanly (as if the PID were real and no longer running). 137 cmd := exec.Command("flock", s.ctx.ClientExitFileLock(), "-c", "sleep 5s") 138 c.Assert(cmd.Start(), gc.IsNil) 139 ch := make(chan error) 140 go func() { 141 ch <- session.RunHook(hookName, s.tmpdir, os.Environ()) 142 }() 143 144 // Wait until either we find the debug dir, or the flock is released. 145 ticker := time.Tick(10 * time.Millisecond) 146 var debugdir os.FileInfo 147 for debugdir == nil { 148 select { 149 case err = <-ch: 150 // flock was released before we found the debug dir. 151 c.Error("could not find hook.sh") 152 153 case <-ticker: 154 tmpdir, err := os.Open(s.tmpdir) 155 if err != nil { 156 c.Fatalf("Failed to open $TMPDIR: %s", err) 157 } 158 fi, err := tmpdir.Readdir(-1) 159 if err != nil { 160 c.Fatalf("Failed to read $TMPDIR: %s", err) 161 } 162 tmpdir.Close() 163 for _, fi := range fi { 164 if fi.IsDir() { 165 hooksh := filepath.Join(s.tmpdir, fi.Name(), "hook.sh") 166 if _, err = os.Stat(hooksh); err == nil { 167 debugdir = fi 168 break 169 } 170 } 171 } 172 if debugdir != nil { 173 break 174 } 175 time.Sleep(10 * time.Millisecond) 176 } 177 } 178 179 envsh := filepath.Join(s.tmpdir, debugdir.Name(), "env.sh") 180 s.verifyEnvshFile(c, envsh, hookName) 181 182 hookpid := filepath.Join(s.tmpdir, debugdir.Name(), "hook.pid") 183 err = ioutil.WriteFile(hookpid, []byte("not a pid"), 0777) 184 c.Assert(err, gc.IsNil) 185 186 // RunHook should complete without waiting to be 187 // killed, and despite the exit lock being held. 188 err = <-ch 189 c.Assert(err, gc.IsNil) 190 cmd.Process.Kill() // kill flock 191 } 192 193 func (s *DebugHooksServerSuite) verifyEnvshFile(c *gc.C, envshPath string, hookName string) { 194 data, err := ioutil.ReadFile(envshPath) 195 c.Assert(err, gc.IsNil) 196 contents := string(data) 197 c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_UNIT_NAME=%q", s.ctx.Unit)) 198 c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_HOOK_NAME=%q", hookName)) 199 c.Assert(contents, jc.Contains, fmt.Sprintf(`PS1="%s:%s %% "`, s.ctx.Unit, hookName)) 200 }