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  }