github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/mongo/mongo_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package mongo_test
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"regexp"
    14  	"runtime"
    15  	"strings"
    16  	stdtesting "testing"
    17  
    18  	"github.com/juju/errors"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/testing"
    21  	jc "github.com/juju/testing/checkers"
    22  	"github.com/juju/utils"
    23  	"github.com/juju/utils/packaging/manager"
    24  	gc "gopkg.in/check.v1"
    25  
    26  	"github.com/juju/juju/mongo"
    27  	"github.com/juju/juju/network"
    28  	"github.com/juju/juju/service/common"
    29  	svctesting "github.com/juju/juju/service/common/testing"
    30  	coretesting "github.com/juju/juju/testing"
    31  	"github.com/juju/juju/version"
    32  )
    33  
    34  func Test(t *stdtesting.T) {
    35  	//TODO(bogdanteleaga): Fix these on windows
    36  	if runtime.GOOS == "windows" {
    37  		t.Skip("bug 1403084: Skipping for now on windows")
    38  	}
    39  	gc.TestingT(t)
    40  }
    41  
    42  type MongoSuite struct {
    43  	coretesting.BaseSuite
    44  	mongodConfigPath string
    45  	mongodPath       string
    46  
    47  	data *svctesting.FakeServiceData
    48  }
    49  
    50  var _ = gc.Suite(&MongoSuite{})
    51  
    52  var testInfo = struct {
    53  	StatePort    int
    54  	Cert         string
    55  	PrivateKey   string
    56  	SharedSecret string
    57  }{
    58  	StatePort:    25252,
    59  	Cert:         "foobar-cert",
    60  	PrivateKey:   "foobar-privkey",
    61  	SharedSecret: "foobar-sharedsecret",
    62  }
    63  
    64  func makeEnsureServerParams(dataDir, namespace string) mongo.EnsureServerParams {
    65  	return mongo.EnsureServerParams{
    66  		StatePort:    testInfo.StatePort,
    67  		Cert:         testInfo.Cert,
    68  		PrivateKey:   testInfo.PrivateKey,
    69  		SharedSecret: testInfo.SharedSecret,
    70  
    71  		DataDir:   dataDir,
    72  		Namespace: namespace,
    73  	}
    74  }
    75  
    76  func (s *MongoSuite) SetUpTest(c *gc.C) {
    77  	s.BaseSuite.SetUpTest(c)
    78  	// Try to make sure we don't execute any commands accidentally.
    79  	s.PatchEnvironment("PATH", "")
    80  
    81  	s.mongodPath = filepath.Join(c.MkDir(), "mongod")
    82  	err := ioutil.WriteFile(s.mongodPath, []byte("#!/bin/bash\n\nprintf %s 'db version v2.4.9'\n"), 0755)
    83  	c.Assert(err, jc.ErrorIsNil)
    84  	s.PatchValue(&mongo.JujuMongodPath, s.mongodPath)
    85  
    86  	// Patch "df" such that it always reports there's 1MB free.
    87  	s.PatchValue(mongo.AvailSpace, func(dir string) (float64, error) {
    88  		info, err := os.Stat(dir)
    89  		if err != nil {
    90  			return 0, err
    91  		}
    92  		if info.IsDir() {
    93  			return 1, nil
    94  
    95  		}
    96  		return 0, fmt.Errorf("not a directory")
    97  	})
    98  	s.PatchValue(mongo.MinOplogSizeMB, 1)
    99  
   100  	testPath := c.MkDir()
   101  	s.mongodConfigPath = filepath.Join(testPath, "mongodConfig")
   102  	s.PatchValue(mongo.MongoConfigPath, s.mongodConfigPath)
   103  
   104  	s.data = svctesting.NewFakeServiceData()
   105  	mongo.PatchService(s.PatchValue, s.data)
   106  }
   107  
   108  func (s *MongoSuite) TestJujuMongodPath(c *gc.C) {
   109  	obtained, err := mongo.Path()
   110  	c.Check(err, jc.ErrorIsNil)
   111  	c.Check(obtained, gc.Equals, s.mongodPath)
   112  }
   113  
   114  func (s *MongoSuite) TestDefaultMongodPath(c *gc.C) {
   115  	s.PatchValue(&mongo.JujuMongodPath, "/not/going/to/exist/mongod")
   116  	s.PatchEnvPathPrepend(filepath.Dir(s.mongodPath))
   117  
   118  	obtained, err := mongo.Path()
   119  	c.Check(err, jc.ErrorIsNil)
   120  	c.Check(obtained, gc.Equals, s.mongodPath)
   121  }
   122  
   123  func (s *MongoSuite) TestMakeJournalDirs(c *gc.C) {
   124  	dir := c.MkDir()
   125  	err := mongo.MakeJournalDirs(dir)
   126  	c.Assert(err, jc.ErrorIsNil)
   127  
   128  	testJournalDirs(dir, c)
   129  }
   130  
   131  func testJournalDirs(dir string, c *gc.C) {
   132  	journalDir := path.Join(dir, "journal")
   133  
   134  	c.Assert(journalDir, jc.IsDirectory)
   135  	info, err := os.Stat(filepath.Join(journalDir, "prealloc.0"))
   136  	c.Assert(err, jc.ErrorIsNil)
   137  
   138  	size := int64(1024 * 1024)
   139  
   140  	c.Assert(info.Size(), gc.Equals, size)
   141  	info, err = os.Stat(filepath.Join(journalDir, "prealloc.1"))
   142  	c.Assert(err, jc.ErrorIsNil)
   143  	c.Assert(info.Size(), gc.Equals, size)
   144  	info, err = os.Stat(filepath.Join(journalDir, "prealloc.2"))
   145  	c.Assert(err, jc.ErrorIsNil)
   146  	c.Assert(info.Size(), gc.Equals, size)
   147  }
   148  
   149  func (s *MongoSuite) TestEnsureServer(c *gc.C) {
   150  	dataDir := s.testEnsureServerNumaCtl(c, false)
   151  
   152  	contents, err := ioutil.ReadFile(s.mongodConfigPath)
   153  	c.Assert(err, jc.ErrorIsNil)
   154  	c.Assert(contents, jc.DeepEquals, []byte("ENABLE_MONGODB=no"))
   155  
   156  	contents, err = ioutil.ReadFile(mongo.SSLKeyPath(dataDir))
   157  	c.Assert(err, jc.ErrorIsNil)
   158  	c.Assert(string(contents), gc.Equals, testInfo.Cert+"\n"+testInfo.PrivateKey)
   159  
   160  	contents, err = ioutil.ReadFile(mongo.SharedSecretPath(dataDir))
   161  	c.Assert(err, jc.ErrorIsNil)
   162  	c.Assert(string(contents), gc.Equals, testInfo.SharedSecret)
   163  
   164  	// make sure that we log the version of mongodb as we get ready to
   165  	// start it
   166  	tlog := c.GetTestLog()
   167  	any := `(.|\n)*`
   168  	start := "^" + any
   169  	tail := any + "$"
   170  	c.Assert(tlog, gc.Matches, start+`using mongod: .*/mongod --version: "db version v2\.4\.9`+tail)
   171  }
   172  
   173  func (s *MongoSuite) TestEnsureServerServerExistsAndRunning(c *gc.C) {
   174  	dataDir := c.MkDir()
   175  	namespace := "namespace"
   176  
   177  	mockShellCommand(c, &s.CleanupSuite, "apt-get")
   178  
   179  	s.data.SetStatus(mongo.ServiceName(namespace), "running")
   180  	s.data.SetErrors(nil, nil, nil, errors.New("shouldn't be called"))
   181  
   182  	err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace))
   183  	c.Assert(err, jc.ErrorIsNil)
   184  
   185  	c.Check(s.data.Installed(), gc.HasLen, 0)
   186  	s.data.CheckCallNames(c, "Installed", "Exists", "Running")
   187  }
   188  
   189  func (s *MongoSuite) TestEnsureServerServerExistsNotRunningIsStarted(c *gc.C) {
   190  	dataDir := c.MkDir()
   191  	namespace := "namespace"
   192  
   193  	mockShellCommand(c, &s.CleanupSuite, "apt-get")
   194  
   195  	s.data.SetStatus(mongo.ServiceName(namespace), "installed")
   196  
   197  	err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace))
   198  	c.Assert(err, jc.ErrorIsNil)
   199  
   200  	c.Check(s.data.Installed(), gc.HasLen, 0)
   201  	s.data.CheckCallNames(c, "Installed", "Exists", "Running", "Start")
   202  }
   203  
   204  func (s *MongoSuite) TestEnsureServerServerExistsNotRunningStartError(c *gc.C) {
   205  	dataDir := c.MkDir()
   206  	namespace := "namespace"
   207  
   208  	mockShellCommand(c, &s.CleanupSuite, "apt-get")
   209  
   210  	s.data.SetStatus(mongo.ServiceName(namespace), "installed")
   211  	failure := errors.New("won't start")
   212  	s.data.SetErrors(nil, nil, nil, failure) // Installed, Exists, Running, Running, Start
   213  
   214  	err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace))
   215  
   216  	c.Check(errors.Cause(err), gc.Equals, failure)
   217  	c.Check(s.data.Installed(), gc.HasLen, 0)
   218  	s.data.CheckCallNames(c, "Installed", "Exists", "Running", "Start")
   219  }
   220  
   221  func (s *MongoSuite) TestEnsureServerNumaCtl(c *gc.C) {
   222  	s.testEnsureServerNumaCtl(c, true)
   223  }
   224  
   225  func (s *MongoSuite) testEnsureServerNumaCtl(c *gc.C, setNumaPolicy bool) string {
   226  	dataDir := c.MkDir()
   227  	dbDir := filepath.Join(dataDir, "db")
   228  	namespace := "namespace"
   229  
   230  	mockShellCommand(c, &s.CleanupSuite, "apt-get")
   231  
   232  	testParams := makeEnsureServerParams(dataDir, namespace)
   233  	testParams.SetNumaControlPolicy = setNumaPolicy
   234  	err := mongo.EnsureServer(testParams)
   235  	c.Assert(err, jc.ErrorIsNil)
   236  
   237  	testJournalDirs(dbDir, c)
   238  
   239  	assertInstalled := func() {
   240  		installed := s.data.Installed()
   241  		c.Assert(installed, gc.HasLen, 1)
   242  		service := installed[0]
   243  		c.Assert(service.Name(), gc.Equals, "juju-db-namespace")
   244  		c.Assert(service.Conf().Desc, gc.Equals, "juju state database")
   245  		if setNumaPolicy {
   246  			stripped := strings.Replace(service.Conf().ExtraScript, "\n", "", -1)
   247  			c.Assert(stripped, gc.Matches, `.* sysctl .*`)
   248  		} else {
   249  			c.Assert(service.Conf().ExtraScript, gc.Equals, "")
   250  		}
   251  		c.Assert(service.Conf().ExecStart, gc.Matches, ".*"+regexp.QuoteMeta(s.mongodPath)+".*")
   252  		c.Assert(service.Conf().Logfile, gc.Equals, "")
   253  	}
   254  	assertInstalled()
   255  	return dataDir
   256  }
   257  
   258  func (s *MongoSuite) TestInstallMongod(c *gc.C) {
   259  	type installs struct {
   260  		series string
   261  		pkg    string
   262  	}
   263  	tests := []installs{
   264  		{"precise", "mongodb-server"},
   265  		{"quantal", "mongodb-server"},
   266  		{"raring", "mongodb-server"},
   267  		{"saucy", "mongodb-server"},
   268  		{"trusty", "juju-mongodb"},
   269  		{"u-series", "juju-mongodb"},
   270  	}
   271  
   272  	mockShellCommand(c, &s.CleanupSuite, "add-apt-repository")
   273  	output := mockShellCommand(c, &s.CleanupSuite, "apt-get")
   274  	for _, test := range tests {
   275  		c.Logf("Testing %s", test.series)
   276  		dataDir := c.MkDir()
   277  		namespace := "namespace" + test.series
   278  
   279  		s.PatchValue(&version.Current.Series, test.series)
   280  
   281  		err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace))
   282  		c.Assert(err, jc.ErrorIsNil)
   283  
   284  		cmds := getMockShellCalls(c, output)
   285  
   286  		// quantal does an extra apt-get install for python software properties
   287  		// so we need to remember to skip that one
   288  		index := 0
   289  		if test.series == "quantal" {
   290  			index = 1
   291  		}
   292  		match := fmt.Sprintf(`.* install .*%s`, test.pkg)
   293  		c.Assert(strings.Join(cmds[index], " "), gc.Matches, match)
   294  		// remove the temp file between tests
   295  		c.Assert(os.Remove(output), gc.IsNil)
   296  	}
   297  }
   298  
   299  func (s *MongoSuite) TestMongoAptGetFails(c *gc.C) {
   300  	s.PatchValue(&version.Current.Series, "trusty")
   301  
   302  	// Any exit code from apt-get that isn't 0 or 100 will be treated
   303  	// as unexpected, skipping the normal retry loop. failCmd causes
   304  	// the command to exit with 1.
   305  	binDir := c.MkDir()
   306  	s.PatchEnvPathPrepend(binDir)
   307  	failCmd(filepath.Join(binDir, "apt-get"))
   308  
   309  	// Set the mongodb service as installed but not running.
   310  	namespace := "namespace"
   311  	s.data.SetStatus(mongo.ServiceName(namespace), "installed")
   312  
   313  	var tw loggo.TestWriter
   314  	c.Assert(loggo.RegisterWriter("test-writer", &tw, loggo.ERROR), jc.ErrorIsNil)
   315  	defer loggo.RemoveWriter("test-writer")
   316  
   317  	dataDir := c.MkDir()
   318  	err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace))
   319  
   320  	// Even though apt-get failed, EnsureServer should continue and
   321  	// not return the error - even though apt-get failed, the Juju
   322  	// mongodb package is most likely already installed.
   323  	// The error should be logged however.
   324  	c.Assert(err, jc.ErrorIsNil)
   325  
   326  	c.Check(tw.Log(), jc.LogMatches, []jc.SimpleMessage{
   327  		{loggo.ERROR, `packaging command failed: .+`},
   328  		{loggo.ERROR, `cannot install/upgrade mongod \(will proceed anyway\): packaging command failed`},
   329  	})
   330  
   331  	// Verify that EnsureServer continued and started the mongodb service.
   332  	c.Check(s.data.Installed(), gc.HasLen, 0)
   333  	s.data.CheckCallNames(c, "Installed", "Exists", "Running", "Start")
   334  }
   335  
   336  func (s *MongoSuite) TestInstallMongodServiceExists(c *gc.C) {
   337  	output := mockShellCommand(c, &s.CleanupSuite, "apt-get")
   338  	dataDir := c.MkDir()
   339  	namespace := "namespace"
   340  
   341  	s.data.SetStatus(mongo.ServiceName(namespace), "running")
   342  	s.data.SetErrors(nil, nil, nil, errors.New("shouldn't be called"))
   343  
   344  	err := mongo.EnsureServer(makeEnsureServerParams(dataDir, namespace))
   345  	c.Assert(err, jc.ErrorIsNil)
   346  
   347  	c.Check(s.data.Installed(), gc.HasLen, 0)
   348  	s.data.CheckCallNames(c, "Installed", "Exists", "Running")
   349  
   350  	// We still attempt to install mongodb, despite the service existing.
   351  	cmds := getMockShellCalls(c, output)
   352  	c.Check(cmds, gc.HasLen, 1)
   353  }
   354  
   355  func (s *MongoSuite) TestNewServiceWithReplSet(c *gc.C) {
   356  	dataDir := c.MkDir()
   357  
   358  	conf := mongo.NewConf(dataDir, dataDir, mongo.JujuMongodPath, 1234, 1024, false)
   359  	c.Assert(strings.Contains(conf.ExecStart, "--replSet"), jc.IsTrue)
   360  }
   361  
   362  func (s *MongoSuite) TestNewServiceWithNumCtl(c *gc.C) {
   363  	dataDir := c.MkDir()
   364  
   365  	conf := mongo.NewConf(dataDir, dataDir, mongo.JujuMongodPath, 1234, 1024, true)
   366  	c.Assert(conf.ExtraScript, gc.Not(gc.Matches), "")
   367  }
   368  
   369  func (s *MongoSuite) TestNewServiceIPv6(c *gc.C) {
   370  	dataDir := c.MkDir()
   371  
   372  	conf := mongo.NewConf(dataDir, dataDir, mongo.JujuMongodPath, 1234, 1024, false)
   373  	c.Assert(strings.Contains(conf.ExecStart, "--ipv6"), jc.IsTrue)
   374  }
   375  
   376  func (s *MongoSuite) TestNewServiceWithJournal(c *gc.C) {
   377  	dataDir := c.MkDir()
   378  
   379  	conf := mongo.NewConf(dataDir, dataDir, mongo.JujuMongodPath, 1234, 1024, false)
   380  	c.Assert(conf.ExecStart, gc.Matches, `.* --journal.*`)
   381  }
   382  
   383  func (s *MongoSuite) TestNoAuthCommandWithJournal(c *gc.C) {
   384  	dataDir := c.MkDir()
   385  
   386  	cmd, err := mongo.NoauthCommand(dataDir, 1234)
   387  	c.Assert(err, jc.ErrorIsNil)
   388  	var isJournalPresent bool
   389  	for _, value := range cmd.Args {
   390  		if value == "--journal" {
   391  			isJournalPresent = true
   392  		}
   393  	}
   394  	c.Assert(isJournalPresent, jc.IsTrue)
   395  }
   396  
   397  func (s *MongoSuite) TestRemoveService(c *gc.C) {
   398  	namespace := "namespace"
   399  	s.data.SetStatus(mongo.ServiceName(namespace), "running")
   400  
   401  	err := mongo.RemoveService(namespace)
   402  	c.Assert(err, jc.ErrorIsNil)
   403  
   404  	removed := s.data.Removed()
   405  	if !c.Check(removed, gc.HasLen, 1) {
   406  		c.Check(removed[0].Name(), gc.Equals, "juju-db-namespace")
   407  		c.Check(removed[0].Conf(), jc.DeepEquals, common.Conf{})
   408  	}
   409  	s.data.CheckCallNames(c, "Stop", "Remove")
   410  }
   411  
   412  func (s *MongoSuite) TestQuantalAptAddRepo(c *gc.C) {
   413  	dir := c.MkDir()
   414  	// patch manager.RunCommandWithRetry for repository addition:
   415  	s.PatchValue(&manager.RunCommandWithRetry, func(string) (string, int, error) {
   416  		return "", 1, fmt.Errorf("packaging command failed: exit status 1")
   417  	})
   418  	s.PatchEnvPathPrepend(dir)
   419  	failCmd(filepath.Join(dir, "add-apt-repository"))
   420  	mockShellCommand(c, &s.CleanupSuite, "apt-get")
   421  
   422  	var tw loggo.TestWriter
   423  	c.Assert(loggo.RegisterWriter("test-writer", &tw, loggo.ERROR), jc.ErrorIsNil)
   424  	defer loggo.RemoveWriter("test-writer")
   425  
   426  	// test that we call add-apt-repository only for quantal
   427  	// (and that if it fails, we log the error)
   428  	s.PatchValue(&version.Current.Series, "quantal")
   429  	err := mongo.EnsureServer(makeEnsureServerParams(dir, ""))
   430  	c.Assert(err, jc.ErrorIsNil)
   431  
   432  	c.Assert(tw.Log(), jc.LogMatches, []jc.SimpleMessage{
   433  		{loggo.ERROR, `cannot install/upgrade mongod \(will proceed anyway\): packaging command failed`},
   434  	})
   435  
   436  	s.PatchValue(&manager.RunCommandWithRetry, func(string) (string, int, error) {
   437  		return "", 0, nil
   438  	})
   439  	s.PatchValue(&version.Current.Series, "trusty")
   440  	failCmd(filepath.Join(dir, "mongod"))
   441  	err = mongo.EnsureServer(makeEnsureServerParams(dir, ""))
   442  	c.Assert(err, jc.ErrorIsNil)
   443  }
   444  
   445  func (s *MongoSuite) TestNoMongoDir(c *gc.C) {
   446  	// Make a non-existent directory that can nonetheless be
   447  	// created.
   448  	mockShellCommand(c, &s.CleanupSuite, "apt-get")
   449  	dataDir := filepath.Join(c.MkDir(), "dir", "data")
   450  	err := mongo.EnsureServer(makeEnsureServerParams(dataDir, ""))
   451  	c.Check(err, jc.ErrorIsNil)
   452  
   453  	_, err = os.Stat(filepath.Join(dataDir, "db"))
   454  	c.Assert(err, jc.ErrorIsNil)
   455  }
   456  
   457  func (s *MongoSuite) TestServiceName(c *gc.C) {
   458  	name := mongo.ServiceName("foo")
   459  	c.Assert(name, gc.Equals, "juju-db-foo")
   460  	name = mongo.ServiceName("")
   461  	c.Assert(name, gc.Equals, "juju-db")
   462  }
   463  
   464  func (s *MongoSuite) TestSelectPeerAddress(c *gc.C) {
   465  	addresses := []network.Address{{
   466  		Value:       "10.0.0.1",
   467  		Type:        network.IPv4Address,
   468  		NetworkName: "cloud",
   469  		Scope:       network.ScopeCloudLocal}, {
   470  		Value:       "8.8.8.8",
   471  		Type:        network.IPv4Address,
   472  		NetworkName: "public",
   473  		Scope:       network.ScopePublic}}
   474  
   475  	address := mongo.SelectPeerAddress(addresses)
   476  	c.Assert(address, gc.Equals, "10.0.0.1")
   477  }
   478  
   479  func (s *MongoSuite) TestSelectPeerHostPort(c *gc.C) {
   480  
   481  	hostPorts := []network.HostPort{{
   482  		Address: network.Address{
   483  			Value:       "10.0.0.1",
   484  			Type:        network.IPv4Address,
   485  			NetworkName: "cloud",
   486  			Scope:       network.ScopeCloudLocal,
   487  		},
   488  		Port: 37017}, {
   489  		Address: network.Address{
   490  			Value:       "8.8.8.8",
   491  			Type:        network.IPv4Address,
   492  			NetworkName: "public",
   493  			Scope:       network.ScopePublic,
   494  		},
   495  		Port: 37017}}
   496  
   497  	address := mongo.SelectPeerHostPort(hostPorts)
   498  	c.Assert(address, gc.Equals, "10.0.0.1:37017")
   499  }
   500  
   501  func (s *MongoSuite) TestGenerateSharedSecret(c *gc.C) {
   502  	secret, err := mongo.GenerateSharedSecret()
   503  	c.Assert(err, jc.ErrorIsNil)
   504  	c.Assert(secret, gc.HasLen, 1024)
   505  	_, err = base64.StdEncoding.DecodeString(secret)
   506  	c.Assert(err, jc.ErrorIsNil)
   507  }
   508  
   509  func (s *MongoSuite) TestAddPPAInQuantal(c *gc.C) {
   510  	mockShellCommand(c, &s.CleanupSuite, "apt-get")
   511  
   512  	addAptRepoOut := mockShellCommand(c, &s.CleanupSuite, "add-apt-repository")
   513  	s.PatchValue(&version.Current.Series, "quantal")
   514  
   515  	dataDir := c.MkDir()
   516  	err := mongo.EnsureServer(makeEnsureServerParams(dataDir, ""))
   517  	c.Assert(err, jc.ErrorIsNil)
   518  
   519  	c.Assert(getMockShellCalls(c, addAptRepoOut), gc.DeepEquals, [][]string{{
   520  		"--yes",
   521  		"\"ppa:juju/stable\"",
   522  	}})
   523  }
   524  
   525  // mockShellCommand creates a new command with the given
   526  // name and contents, and patches $PATH so that it will be
   527  // executed by preference. It returns the name of a file
   528  // that is written by each call to the command - mockShellCalls
   529  // can be used to retrieve the calls.
   530  func mockShellCommand(c *gc.C, s *testing.CleanupSuite, name string) string {
   531  	dir := c.MkDir()
   532  	s.PatchEnvPathPrepend(dir)
   533  
   534  	// Note the shell script produces output of the form:
   535  	// +arg1+\n
   536  	// +arg2+\n
   537  	// ...
   538  	// +argn+\n
   539  	// -
   540  	//
   541  	// It would be nice if there was a simple way of unambiguously
   542  	// quoting shell arguments, but this will do as long
   543  	// as no argument contains a newline character.
   544  	outputFile := filepath.Join(dir, name+".out")
   545  	contents := `#!/bin/sh
   546  {
   547  	for i in "$@"; do
   548  		echo +"$i"+
   549  	done
   550  	echo -
   551  } >> ` + utils.ShQuote(outputFile) + `
   552  `
   553  	err := ioutil.WriteFile(filepath.Join(dir, name), []byte(contents), 0755)
   554  	c.Assert(err, jc.ErrorIsNil)
   555  	return outputFile
   556  }
   557  
   558  // getMockShellCalls, given a file name returned by mockShellCommands,
   559  // returns a slice containing one element for each call, each
   560  // containing the arguments passed to the command.
   561  // It will be confused if the arguments contain newlines.
   562  func getMockShellCalls(c *gc.C, file string) [][]string {
   563  	data, err := ioutil.ReadFile(file)
   564  	if os.IsNotExist(err) {
   565  		return nil
   566  	}
   567  	c.Assert(err, jc.ErrorIsNil)
   568  	s := string(data)
   569  	parts := strings.Split(s, "\n-\n")
   570  	c.Assert(parts[len(parts)-1], gc.Equals, "")
   571  	var calls [][]string
   572  	for _, part := range parts[0 : len(parts)-1] {
   573  		calls = append(calls, splitCall(c, part))
   574  	}
   575  	return calls
   576  }
   577  
   578  // splitCall splits the output produced by a single call to the
   579  // mocked shell function (see mockShellCommand) and
   580  // splits it into its individual arguments.
   581  func splitCall(c *gc.C, part string) []string {
   582  	var result []string
   583  	for _, arg := range strings.Split(part, "\n") {
   584  		c.Assert(arg, gc.Matches, `\+.*\+`)
   585  		arg = strings.TrimSuffix(arg, "+")
   586  		arg = strings.TrimPrefix(arg, "+")
   587  		result = append(result, arg)
   588  	}
   589  	return result
   590  }
   591  
   592  // failCmd creates an executable file at the given location that will do nothing
   593  // except return an error.
   594  func failCmd(path string) {
   595  	err := ioutil.WriteFile(path, []byte("#!/bin/bash --norc\nexit 1"), 0755)
   596  	if err != nil {
   597  		panic(err)
   598  	}
   599  }