github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/uniter/uniter_test.go (about)

     1  // Copyright 2012-2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package uniter_test
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"syscall"
    15  	"time"
    16  
    17  	"github.com/juju/clock/testclock"
    18  	"github.com/juju/errors"
    19  	jc "github.com/juju/testing/checkers"
    20  	ft "github.com/juju/testing/filetesting"
    21  	gc "gopkg.in/check.v1"
    22  	corecharm "gopkg.in/juju/charm.v6"
    23  
    24  	"github.com/juju/juju/agent/tools"
    25  	"github.com/juju/juju/apiserver/params"
    26  	"github.com/juju/juju/component/all"
    27  	"github.com/juju/juju/core/status"
    28  	"github.com/juju/juju/juju/testing"
    29  	"github.com/juju/juju/state"
    30  	"github.com/juju/juju/testcharms"
    31  	coretesting "github.com/juju/juju/testing"
    32  	"github.com/juju/juju/worker/uniter/operation"
    33  )
    34  
    35  type UniterSuite struct {
    36  	coretesting.GitSuite
    37  	testing.JujuConnSuite
    38  	dataDir  string
    39  	oldLcAll string
    40  	unitDir  string
    41  
    42  	updateStatusHookTicker *manualTicker
    43  }
    44  
    45  var _ = gc.Suite(&UniterSuite{})
    46  
    47  var leaseClock *testclock.Clock
    48  
    49  // This guarantees that we get proper platform
    50  // specific error directly from their source
    51  // This works on both windows and unix
    52  var errNotDir = syscall.ENOTDIR.Error()
    53  
    54  func (s *UniterSuite) SetUpSuite(c *gc.C) {
    55  	s.GitSuite.SetUpSuite(c)
    56  	s.JujuConnSuite.SetUpSuite(c)
    57  	s.dataDir = c.MkDir()
    58  	toolsDir := tools.ToolsDir(s.dataDir, "unit-u-0")
    59  	err := os.MkdirAll(toolsDir, 0755)
    60  	c.Assert(err, jc.ErrorIsNil)
    61  	// TODO(fwereade) GAAAAAAAAAAAAAAAAAH this is LUDICROUS.
    62  	cmd := exec.Command(jujudBuildArgs[0], jujudBuildArgs[1:]...)
    63  	cmd.Dir = toolsDir
    64  	out, err := cmd.CombinedOutput()
    65  	c.Logf(string(out))
    66  	c.Assert(err, jc.ErrorIsNil)
    67  	s.oldLcAll = os.Getenv("LC_ALL")
    68  	os.Setenv("LC_ALL", "en_US")
    69  	s.unitDir = filepath.Join(s.dataDir, "agents", "unit-u-0")
    70  	all.RegisterForServer()
    71  }
    72  
    73  func (s *UniterSuite) TearDownSuite(c *gc.C) {
    74  	os.Setenv("LC_ALL", s.oldLcAll)
    75  	s.JujuConnSuite.TearDownSuite(c)
    76  	s.GitSuite.TearDownSuite(c)
    77  }
    78  
    79  func (s *UniterSuite) SetUpTest(c *gc.C) {
    80  	zone, err := time.LoadLocation("")
    81  	c.Assert(err, jc.ErrorIsNil)
    82  	now := time.Date(2030, 11, 11, 11, 11, 11, 11, zone)
    83  	leaseClock = testclock.NewClock(now)
    84  	s.updateStatusHookTicker = newManualTicker()
    85  	s.GitSuite.SetUpTest(c)
    86  	s.JujuConnSuite.SetUpTest(c)
    87  	err = s.State.SetClockForTesting(leaseClock)
    88  	c.Assert(err, jc.ErrorIsNil)
    89  }
    90  
    91  func (s *UniterSuite) TearDownTest(c *gc.C) {
    92  	s.ResetContext(c)
    93  	s.JujuConnSuite.TearDownTest(c)
    94  	s.GitSuite.TearDownTest(c)
    95  }
    96  
    97  func (s *UniterSuite) Reset(c *gc.C) {
    98  	s.JujuConnSuite.Reset(c)
    99  	s.ResetContext(c)
   100  }
   101  
   102  func (s *UniterSuite) ResetContext(c *gc.C) {
   103  	err := os.RemoveAll(s.unitDir)
   104  	c.Assert(err, jc.ErrorIsNil)
   105  }
   106  
   107  func (s *UniterSuite) runUniterTests(c *gc.C, uniterTests []uniterTest) {
   108  	for i, t := range uniterTests {
   109  		c.Logf("\ntest %d: %s\n", i, t.summary)
   110  		func() {
   111  			defer s.Reset(c)
   112  
   113  			ctx := &context{
   114  				s:                      s,
   115  				st:                     s.State,
   116  				uuid:                   s.State.ModelUUID(),
   117  				path:                   s.unitDir,
   118  				dataDir:                s.dataDir,
   119  				charms:                 make(map[string][]byte),
   120  				leaseManager:           s.LeaseManager,
   121  				updateStatusHookTicker: s.updateStatusHookTicker,
   122  				charmDirGuard:          &mockCharmDirGuard{},
   123  			}
   124  			ctx.run(c, t.steps)
   125  		}()
   126  	}
   127  }
   128  
   129  func (s *UniterSuite) TestUniterStartup(c *gc.C) {
   130  	s.runUniterTests(c, []uniterTest{
   131  		// Check conditions that can cause the uniter to fail to start.
   132  		ut(
   133  			"unable to create state dir",
   134  			writeFile{"state", 0644},
   135  			createCharm{},
   136  			createApplicationAndUnit{},
   137  			startUniter{},
   138  			waitUniterDead{err: `failed to initialize uniter for "unit-u-0": .*` + errNotDir},
   139  		), ut(
   140  			"unknown unit",
   141  			// We still need to create a unit, because that's when we also
   142  			// connect to the API, but here we use a different application
   143  			// (and hence unit) name.
   144  			createCharm{},
   145  			createApplicationAndUnit{applicationName: "w"},
   146  			startUniter{unitTag: "unit-u-0"},
   147  			waitUniterDead{err: `failed to initialize uniter for "unit-u-0": permission denied`},
   148  		),
   149  	})
   150  }
   151  
   152  func (s *UniterSuite) TestPreviousDownloadsCleared(c *gc.C) {
   153  	s.runUniterTests(c, []uniterTest{
   154  		ut(
   155  			"Ensure stale download files are cleared on uniter startup",
   156  			createCharm{},
   157  			serveCharm{},
   158  			ensureStateWorker{},
   159  			createApplicationAndUnit{},
   160  			createDownloads{},
   161  			startUniter{},
   162  			waitAddresses{},
   163  			waitUnitAgent{status: status.Idle},
   164  			verifyDownloadsCleared{},
   165  		),
   166  	})
   167  }
   168  
   169  func (s *UniterSuite) TestUniterBootstrap(c *gc.C) {
   170  	//TODO(bogdanteleaga): Fix this on windows
   171  	if runtime.GOOS == "windows" {
   172  		c.Skip("bug 1403084: currently does not work on windows")
   173  	}
   174  	s.runUniterTests(c, []uniterTest{
   175  		// Check error conditions during unit bootstrap phase.
   176  		ut(
   177  			"insane deployment",
   178  			createCharm{},
   179  			serveCharm{},
   180  			writeFile{"charm", 0644},
   181  			createUniter{},
   182  			waitUniterDead{err: `executing operation "install cs:quantal/wordpress-0": open .*` + errNotDir},
   183  		), ut(
   184  			"charm cannot be downloaded",
   185  			createCharm{},
   186  			// don't serve charm
   187  			createUniter{},
   188  			waitUniterDead{err: `preparing operation "install cs:quantal/wordpress-0": failed to download charm .* not found`},
   189  		),
   190  	})
   191  }
   192  
   193  type noopExecutor struct {
   194  	operation.Executor
   195  }
   196  
   197  func (m *noopExecutor) Run(op operation.Operation) error {
   198  	return errors.New("some error occurred")
   199  }
   200  
   201  func (s *UniterSuite) TestUniterStartupStatus(c *gc.C) {
   202  	executorFunc := func(stateFilePath string, initialState operation.State, acquireLock func(string) (func(), error)) (operation.Executor, error) {
   203  		e, err := operation.NewExecutor(stateFilePath, initialState, acquireLock)
   204  		c.Assert(err, jc.ErrorIsNil)
   205  		return &mockExecutor{e}, nil
   206  	}
   207  	s.runUniterTests(c, []uniterTest{
   208  		ut(
   209  			"unit status and message at startup",
   210  			createCharm{},
   211  			serveCharm{},
   212  			ensureStateWorker{},
   213  			createApplicationAndUnit{},
   214  			startUniter{
   215  				newExecutorFunc: executorFunc,
   216  			},
   217  			waitUnitAgent{
   218  				statusGetter: unitStatusGetter,
   219  				status:       status.Waiting,
   220  				info:         status.MessageInitializingAgent,
   221  			},
   222  			waitUnitAgent{
   223  				status: status.Failed,
   224  				info:   "resolver loop error",
   225  			},
   226  			expectError{".*some error occurred.*"},
   227  		),
   228  	})
   229  }
   230  
   231  func (s *UniterSuite) TestUniterInstallHook(c *gc.C) {
   232  	s.runUniterTests(c, []uniterTest{
   233  		ut(
   234  			"install hook fail and resolve",
   235  			startupError{"install"},
   236  			verifyWaiting{},
   237  
   238  			resolveError{state.ResolvedNoHooks},
   239  			waitUnitAgent{
   240  				status: status.Idle,
   241  			},
   242  			waitUnitAgent{
   243  				statusGetter: unitStatusGetter,
   244  				status:       status.Unknown,
   245  			},
   246  			waitHooks{"leader-elected", "config-changed", "start"},
   247  		), ut(
   248  			"install hook fail and retry",
   249  			startupError{"install"},
   250  			verifyWaiting{},
   251  
   252  			resolveError{state.ResolvedRetryHooks},
   253  			waitUnitAgent{
   254  				statusGetter: unitStatusGetter,
   255  				status:       status.Error,
   256  				info:         `hook failed: "install"`,
   257  				data: map[string]interface{}{
   258  					"hook": "install",
   259  				},
   260  			},
   261  			waitHooks{"fail-install"},
   262  			fixHook{"install"},
   263  			verifyWaiting{},
   264  
   265  			resolveError{state.ResolvedRetryHooks},
   266  			waitUnitAgent{
   267  				status: status.Idle,
   268  			},
   269  			waitHooks{"install", "leader-elected", "config-changed", "start"},
   270  		),
   271  	})
   272  }
   273  
   274  func (s *UniterSuite) TestUniterUpdateStatusHook(c *gc.C) {
   275  	s.runUniterTests(c, []uniterTest{
   276  		ut(
   277  			"update status hook runs on timer",
   278  			createCharm{},
   279  			serveCharm{},
   280  			createUniter{},
   281  			waitHooks(startupHooks(false)),
   282  			waitUnitAgent{status: status.Idle},
   283  			updateStatusHookTick{},
   284  			waitHooks{"update-status"},
   285  		),
   286  	})
   287  }
   288  
   289  func (s *UniterSuite) TestNoUniterUpdateStatusHookInError(c *gc.C) {
   290  	s.runUniterTests(c, []uniterTest{
   291  		ut(
   292  			"update status hook doesn't run if in error",
   293  			startupError{"start"},
   294  			waitHooks{},
   295  			updateStatusHookTick{},
   296  			waitHooks{},
   297  
   298  			// Resolve and hook should run.
   299  			resolveError{state.ResolvedNoHooks},
   300  			waitUnitAgent{
   301  				status: status.Idle,
   302  			},
   303  			waitHooks{},
   304  			updateStatusHookTick{},
   305  			waitHooks{"update-status"},
   306  		),
   307  	})
   308  }
   309  
   310  func (s *UniterSuite) TestUniterStartHook(c *gc.C) {
   311  	s.runUniterTests(c, []uniterTest{
   312  		ut(
   313  			"start hook fail and resolve",
   314  			startupError{"start"},
   315  			verifyWaiting{},
   316  
   317  			resolveError{state.ResolvedNoHooks},
   318  			waitUnitAgent{
   319  				status: status.Idle,
   320  			},
   321  			waitUnitAgent{
   322  				statusGetter: unitStatusGetter,
   323  				status:       status.Maintenance,
   324  				info:         "installing charm software",
   325  			},
   326  			waitHooks{},
   327  			verifyRunning{},
   328  		), ut(
   329  			"start hook fail and retry",
   330  			startupError{"start"},
   331  			verifyWaiting{},
   332  
   333  			resolveError{state.ResolvedRetryHooks},
   334  			waitUnitAgent{
   335  				statusGetter: unitStatusGetter,
   336  				status:       status.Error,
   337  				info:         `hook failed: "start"`,
   338  				data: map[string]interface{}{
   339  					"hook": "start",
   340  				},
   341  			},
   342  			waitHooks{"fail-start"},
   343  			verifyWaiting{},
   344  
   345  			fixHook{"start"},
   346  			resolveError{state.ResolvedRetryHooks},
   347  			waitUnitAgent{
   348  				status: status.Idle,
   349  			},
   350  			waitHooks{"start"},
   351  			verifyRunning{},
   352  		),
   353  	})
   354  }
   355  
   356  func (s *UniterSuite) TestUniterMultipleErrors(c *gc.C) {
   357  	s.runUniterTests(c, []uniterTest{
   358  		ut(
   359  			"resolved is cleared before moving on to next hook",
   360  			createCharm{badHooks: []string{"install", "leader-elected", "config-changed", "start"}},
   361  			serveCharm{},
   362  			createUniter{},
   363  			waitUnitAgent{
   364  				statusGetter: unitStatusGetter,
   365  				status:       status.Error,
   366  				info:         `hook failed: "install"`,
   367  				data: map[string]interface{}{
   368  					"hook": "install",
   369  				},
   370  			},
   371  			resolveError{state.ResolvedNoHooks},
   372  			waitUnitAgent{
   373  				statusGetter: unitStatusGetter,
   374  				status:       status.Error,
   375  				info:         `hook failed: "leader-elected"`,
   376  				data: map[string]interface{}{
   377  					"hook": "leader-elected",
   378  				},
   379  			},
   380  			resolveError{state.ResolvedNoHooks},
   381  			waitUnitAgent{
   382  				statusGetter: unitStatusGetter,
   383  				status:       status.Error,
   384  				info:         `hook failed: "config-changed"`,
   385  				data: map[string]interface{}{
   386  					"hook": "config-changed",
   387  				},
   388  			},
   389  			resolveError{state.ResolvedNoHooks},
   390  			waitUnitAgent{
   391  				statusGetter: unitStatusGetter,
   392  				status:       status.Error,
   393  				info:         `hook failed: "start"`,
   394  				data: map[string]interface{}{
   395  					"hook": "start",
   396  				},
   397  			},
   398  		),
   399  	})
   400  }
   401  
   402  func (s *UniterSuite) TestUniterConfigChangedHook(c *gc.C) {
   403  	s.runUniterTests(c, []uniterTest{
   404  		ut(
   405  			"config-changed hook fail and resolve",
   406  			startupError{"config-changed"},
   407  			verifyWaiting{},
   408  
   409  			// Note: we'll run another config-changed as soon as we hit the
   410  			// started state, so the broken hook would actually prevent us
   411  			// from advancing at all if we didn't fix it.
   412  			fixHook{"config-changed"},
   413  			resolveError{state.ResolvedNoHooks},
   414  			waitUnitAgent{
   415  				status: status.Idle,
   416  			},
   417  			waitUnitAgent{
   418  				statusGetter: unitStatusGetter,
   419  				status:       status.Unknown,
   420  			},
   421  			// TODO(axw) confirm with fwereade that this is correct.
   422  			// Previously we would see "start", "config-changed".
   423  			// I don't think we should see another config-changed,
   424  			// since config did not change since we resolved the
   425  			// failed one above.
   426  			waitHooks{"start"},
   427  			// If we'd accidentally retried that hook, somehow, we would get
   428  			// an extra config-changed as we entered started; see that we don't.
   429  			waitHooks{},
   430  			verifyRunning{},
   431  		), ut(
   432  			"config-changed hook fail and retry",
   433  			startupError{"config-changed"},
   434  			verifyWaiting{},
   435  
   436  			resolveError{state.ResolvedRetryHooks},
   437  			waitUnitAgent{
   438  				statusGetter: unitStatusGetter,
   439  				status:       status.Error,
   440  				info:         `hook failed: "config-changed"`,
   441  				data: map[string]interface{}{
   442  					"hook": "config-changed",
   443  				},
   444  			},
   445  			waitHooks{"fail-config-changed"},
   446  			verifyWaiting{},
   447  
   448  			fixHook{"config-changed"},
   449  			resolveError{state.ResolvedRetryHooks},
   450  			waitUnitAgent{
   451  				status: status.Idle,
   452  			},
   453  			waitHooks{"config-changed", "start"},
   454  			verifyRunning{},
   455  		), ut(
   456  			"steady state config change with config-get verification",
   457  			createCharm{
   458  				customize: func(c *gc.C, ctx *context, path string) {
   459  					appendHook(c, path, "config-changed", appendConfigChanged)
   460  				},
   461  			},
   462  			serveCharm{},
   463  			createUniter{},
   464  			waitUnitAgent{
   465  				status: status.Idle,
   466  			},
   467  			waitHooks{"install", "leader-elected", "config-changed", "start"},
   468  			assertYaml{"charm/config.out", map[string]interface{}{
   469  				"blog-title": "My Title",
   470  			}},
   471  			changeConfig{"blog-title": "Goodness Gracious Me"},
   472  			waitHooks{"config-changed"},
   473  			verifyRunning{},
   474  			assertYaml{"charm/config.out", map[string]interface{}{
   475  				"blog-title": "Goodness Gracious Me",
   476  			}},
   477  		),
   478  	})
   479  }
   480  
   481  func (s *UniterSuite) TestUniterHookSynchronisation(c *gc.C) {
   482  	var lock hookLock
   483  	s.runUniterTests(c, []uniterTest{
   484  		ut(
   485  			"verify config change hook not run while lock held",
   486  			quickStart{},
   487  			lock.acquire(),
   488  			changeConfig{"blog-title": "Goodness Gracious Me"},
   489  			waitHooks{},
   490  			lock.release(),
   491  			waitHooks{"config-changed"},
   492  		), ut(
   493  			"verify held lock by another unit is not broken",
   494  			lock.acquire(),
   495  			// Can't use quickstart as it has a built in waitHooks.
   496  			createCharm{},
   497  			serveCharm{},
   498  			ensureStateWorker{},
   499  			createApplicationAndUnit{},
   500  			startUniter{},
   501  			waitAddresses{},
   502  			waitHooks{},
   503  			lock.release(),
   504  			waitUnitAgent{status: status.Idle},
   505  			waitHooks{"install", "leader-elected", "config-changed", "start"},
   506  		),
   507  	})
   508  }
   509  
   510  func (s *UniterSuite) TestUniterDyingReaction(c *gc.C) {
   511  	s.runUniterTests(c, []uniterTest{
   512  		// Reaction to entity deaths.
   513  		ut(
   514  			"steady state unit dying",
   515  			quickStart{},
   516  			unitDying,
   517  			waitHooks{"leader-settings-changed", "stop"},
   518  			waitUniterDead{},
   519  		), ut(
   520  			"steady state unit dead",
   521  			quickStart{},
   522  			unitDead,
   523  			waitUniterDead{},
   524  			waitHooks{},
   525  		), ut(
   526  			"hook error unit dying",
   527  			startupError{"start"},
   528  			unitDying,
   529  			verifyWaiting{},
   530  			fixHook{"start"},
   531  			resolveError{state.ResolvedRetryHooks},
   532  			waitHooks{"start", "leader-settings-changed", "stop"},
   533  			waitUniterDead{},
   534  		), ut(
   535  			"hook error unit dead",
   536  			startupError{"start"},
   537  			unitDead,
   538  			waitUniterDead{},
   539  			waitHooks{},
   540  		),
   541  	})
   542  }
   543  
   544  func (s *UniterSuite) TestUniterSteadyStateUpgrade(c *gc.C) {
   545  	s.runUniterTests(c, []uniterTest{
   546  		// Upgrade scenarios from steady state.
   547  		ut(
   548  			"steady state upgrade",
   549  			quickStart{},
   550  			createCharm{revision: 1},
   551  			upgradeCharm{revision: 1},
   552  			waitUnitAgent{
   553  				status: status.Idle,
   554  				charm:  1,
   555  			},
   556  			waitUnitAgent{
   557  				statusGetter: unitStatusGetter,
   558  				status:       status.Unknown,
   559  				charm:        1,
   560  			},
   561  			waitHooks{"upgrade-charm", "config-changed"},
   562  			verifyCharm{revision: 1},
   563  			verifyRunning{},
   564  		),
   565  	})
   566  }
   567  
   568  func (s *UniterSuite) TestUniterSteadyStateUpgradeForce(c *gc.C) {
   569  	s.runUniterTests(c, []uniterTest{
   570  		ut(
   571  			"steady state forced upgrade (identical behaviour)",
   572  			quickStart{},
   573  			createCharm{revision: 1},
   574  			upgradeCharm{revision: 1, forced: true},
   575  			waitUnitAgent{
   576  				status: status.Idle,
   577  				charm:  1,
   578  			},
   579  			waitUnitAgent{
   580  				statusGetter: unitStatusGetter,
   581  				status:       status.Unknown,
   582  				charm:        1,
   583  			},
   584  			waitHooks{"upgrade-charm", "config-changed"},
   585  			verifyCharm{revision: 1},
   586  			verifyRunning{},
   587  		),
   588  	})
   589  }
   590  
   591  func (s *UniterSuite) TestUniterSteadyStateUpgradeResolve(c *gc.C) {
   592  	s.runUniterTests(c, []uniterTest{
   593  		ut(
   594  			"steady state upgrade hook fail and resolve",
   595  			quickStart{},
   596  			createCharm{revision: 1, badHooks: []string{"upgrade-charm"}},
   597  			upgradeCharm{revision: 1},
   598  			waitUnitAgent{
   599  				statusGetter: unitStatusGetter,
   600  				status:       status.Error,
   601  				info:         `hook failed: "upgrade-charm"`,
   602  				data: map[string]interface{}{
   603  					"hook": "upgrade-charm",
   604  				},
   605  				charm: 1,
   606  			},
   607  			waitHooks{"fail-upgrade-charm"},
   608  			verifyCharm{revision: 1},
   609  			verifyWaiting{},
   610  
   611  			resolveError{state.ResolvedNoHooks},
   612  			waitUnitAgent{
   613  				status: status.Idle,
   614  				charm:  1,
   615  			},
   616  			waitUnitAgent{
   617  				statusGetter: unitStatusGetter,
   618  				status:       status.Unknown,
   619  				charm:        1,
   620  			},
   621  			waitHooks{"config-changed"},
   622  			verifyRunning{},
   623  		),
   624  	})
   625  }
   626  
   627  func (s *UniterSuite) TestUniterSteadyStateUpgradeRetry(c *gc.C) {
   628  	s.runUniterTests(c, []uniterTest{
   629  		ut(
   630  			"steady state upgrade hook fail and retry",
   631  			quickStart{},
   632  			createCharm{revision: 1, badHooks: []string{"upgrade-charm"}},
   633  			upgradeCharm{revision: 1},
   634  			waitUnitAgent{
   635  				statusGetter: unitStatusGetter,
   636  				status:       status.Error,
   637  				info:         `hook failed: "upgrade-charm"`,
   638  				data: map[string]interface{}{
   639  					"hook": "upgrade-charm",
   640  				},
   641  				charm: 1,
   642  			},
   643  			waitHooks{"fail-upgrade-charm"},
   644  			verifyCharm{revision: 1},
   645  			verifyWaiting{},
   646  
   647  			resolveError{state.ResolvedRetryHooks},
   648  			waitUnitAgent{
   649  				statusGetter: unitStatusGetter,
   650  				status:       status.Error,
   651  				info:         `hook failed: "upgrade-charm"`,
   652  				data: map[string]interface{}{
   653  					"hook": "upgrade-charm",
   654  				},
   655  				charm: 1,
   656  			},
   657  			waitHooks{"fail-upgrade-charm"},
   658  			verifyWaiting{},
   659  
   660  			fixHook{"upgrade-charm"},
   661  			resolveError{state.ResolvedRetryHooks},
   662  			waitUnitAgent{
   663  				status: status.Idle,
   664  				charm:  1,
   665  			},
   666  			waitHooks{"upgrade-charm", "config-changed"},
   667  			verifyRunning{},
   668  		),
   669  	})
   670  }
   671  
   672  func (s *UniterSuite) TestUpdateResourceCausesUpgrade(c *gc.C) {
   673  	// appendStorageMetadata customises the wordpress charm's metadata,
   674  	// adding a "wp-content" filesystem store. We do it here rather
   675  	// than in the charm itself to avoid modifying all of the other
   676  	// scenarios.
   677  	// NOTE: this test does not expect the charm profile data to be
   678  	// in place for the resource charm upgrade. In fact it's important
   679  	// that we don't have it there, otherwise it causes the resource
   680  	// changes to stall out in real world scenarios.
   681  	appendResource := func(c *gc.C, ctx *context, path string) {
   682  		f, err := os.OpenFile(filepath.Join(path, "metadata.yaml"), os.O_RDWR|os.O_APPEND, 0644)
   683  		c.Assert(err, jc.ErrorIsNil)
   684  		defer func() {
   685  			err := f.Close()
   686  			c.Assert(err, jc.ErrorIsNil)
   687  		}()
   688  		_, err = io.WriteString(f, `
   689  resources:
   690    data:
   691      Type: file
   692      filename: filename.tgz
   693      comment: One line that is useful when operators need to push it.`)
   694  		c.Assert(err, jc.ErrorIsNil)
   695  	}
   696  	s.runUniterTests(c, []uniterTest{
   697  		ut(
   698  			"update resource causes upgrade",
   699  
   700  			// These steps are just copied from quickstart with a customized
   701  			// createCharm.
   702  			createCharm{customize: appendResource},
   703  			serveCharm{},
   704  			createUniter{},
   705  			waitUnitAgent{status: status.Idle},
   706  			waitHooks(startupHooks(false)),
   707  			verifyCharm{},
   708  
   709  			pushResource{},
   710  			waitHooks{"upgrade-charm", "config-changed"},
   711  		),
   712  	})
   713  }
   714  
   715  func (s *UniterSuite) TestUniterUpgradeOverwrite(c *gc.C) {
   716  	//TODO(bogdanteleaga): Fix this on windows
   717  	coretesting.SkipIfWindowsBug(c, "lp:1403084")
   718  	//TODO(hml): Fix this on S390X, intermittent there.
   719  	coretesting.SkipIfS390X(c, "lp:1534637")
   720  	makeTest := func(description string, content, extraChecks ft.Entries) uniterTest {
   721  		return ut(description,
   722  			createCharm{
   723  				// This is the base charm which all upgrade tests start out running.
   724  				customize: func(c *gc.C, ctx *context, path string) {
   725  					ft.Entries{
   726  						ft.Dir{"dir", 0755},
   727  						ft.File{"file", "blah", 0644},
   728  						ft.Symlink{"symlink", "file"},
   729  					}.Create(c, path)
   730  					// Note that it creates "dir/user-file" at runtime, which may be
   731  					// preserved or removed depending on the test.
   732  					script := "echo content > dir/user-file && chmod 755 dir/user-file"
   733  					appendHook(c, path, "start", script)
   734  				},
   735  			},
   736  			serveCharm{},
   737  			createUniter{},
   738  			waitUnitAgent{
   739  				status: status.Idle,
   740  			},
   741  			waitUnitAgent{
   742  				statusGetter: unitStatusGetter,
   743  				status:       status.Unknown,
   744  			},
   745  			waitHooks{"install", "leader-elected", "config-changed", "start"},
   746  
   747  			createCharm{
   748  				revision: 1,
   749  				customize: func(c *gc.C, _ *context, path string) {
   750  					content.Create(c, path)
   751  				},
   752  			},
   753  			serveCharm{},
   754  			upgradeCharm{revision: 1},
   755  			waitUnitAgent{
   756  				status: status.Idle,
   757  				charm:  1,
   758  			},
   759  			waitUnitAgent{
   760  				statusGetter: unitStatusGetter,
   761  				status:       status.Unknown,
   762  				charm:        1,
   763  			},
   764  			waitHooks{"upgrade-charm", "config-changed"},
   765  			verifyCharm{revision: 1},
   766  			custom{func(c *gc.C, ctx *context) {
   767  				path := filepath.Join(ctx.path, "charm")
   768  				content.Check(c, path)
   769  				extraChecks.Check(c, path)
   770  			}},
   771  			verifyRunning{},
   772  		)
   773  	}
   774  
   775  	s.runUniterTests(c, []uniterTest{
   776  		makeTest(
   777  			"files overwite files, dirs, symlinks",
   778  			ft.Entries{
   779  				ft.File{"file", "new", 0755},
   780  				ft.File{"dir", "new", 0755},
   781  				ft.File{"symlink", "new", 0755},
   782  			},
   783  			ft.Entries{
   784  				ft.Removed{"dir/user-file"},
   785  			},
   786  		), makeTest(
   787  			"symlinks overwite files, dirs, symlinks",
   788  			ft.Entries{
   789  				ft.Symlink{"file", "new"},
   790  				ft.Symlink{"dir", "new"},
   791  				ft.Symlink{"symlink", "new"},
   792  			},
   793  			ft.Entries{
   794  				ft.Removed{"dir/user-file"},
   795  			},
   796  		), makeTest(
   797  			"dirs overwite files, symlinks; merge dirs",
   798  			ft.Entries{
   799  				ft.Dir{"file", 0755},
   800  				ft.Dir{"dir", 0755},
   801  				ft.File{"dir/charm-file", "charm-content", 0644},
   802  				ft.Dir{"symlink", 0755},
   803  			},
   804  			ft.Entries{
   805  				ft.File{"dir/user-file", "content\n", 0755},
   806  			},
   807  		),
   808  	})
   809  }
   810  
   811  func (s *UniterSuite) TestUniterErrorStateUnforcedUpgrade(c *gc.C) {
   812  	s.runUniterTests(c, []uniterTest{
   813  		// Upgrade scenarios from error state.
   814  		ut(
   815  			"error state unforced upgrade (ignored until started state)",
   816  			startupError{"start"},
   817  			createCharm{revision: 1},
   818  			upgradeCharm{revision: 1},
   819  			waitUnitAgent{
   820  				statusGetter: unitStatusGetter,
   821  				status:       status.Error,
   822  				info:         `hook failed: "start"`,
   823  				data: map[string]interface{}{
   824  					"hook": "start",
   825  				},
   826  			},
   827  			waitHooks{},
   828  			verifyCharm{},
   829  			verifyWaiting{},
   830  
   831  			resolveError{state.ResolvedNoHooks},
   832  			waitUnitAgent{
   833  				status: status.Idle,
   834  				charm:  1,
   835  			},
   836  			waitUnitAgent{
   837  				statusGetter: unitStatusGetter,
   838  				status:       status.Maintenance,
   839  				info:         "installing charm software",
   840  				charm:        1,
   841  			},
   842  			waitHooks{"upgrade-charm", "config-changed"},
   843  			verifyCharm{revision: 1},
   844  			verifyRunning{},
   845  		)})
   846  }
   847  
   848  func (s *UniterSuite) TestUniterErrorStateForcedUpgrade(c *gc.C) {
   849  	s.runUniterTests(c, []uniterTest{
   850  		ut(
   851  			"error state forced upgrade",
   852  			startupError{"start"},
   853  			createCharm{revision: 1},
   854  			upgradeCharm{revision: 1, forced: true},
   855  			// It's not possible to tell directly from state when the upgrade is
   856  			// complete, because the new unit charm URL is set at the upgrade
   857  			// process's point of no return (before actually deploying, but after
   858  			// the charm has been downloaded and verified). However, it's still
   859  			// useful to wait until that point...
   860  			waitUnitAgent{
   861  				statusGetter: unitStatusGetter,
   862  				status:       status.Error,
   863  				info:         `hook failed: "start"`,
   864  				data: map[string]interface{}{
   865  					"hook": "start",
   866  				},
   867  				charm: 1,
   868  			},
   869  			// ...because the uniter *will* complete a started deployment even if
   870  			// we stop it from outside. So, by stopping and starting, we can be
   871  			// sure that the operation has completed and can safely verify that
   872  			// the charm state on disk is as we expect.
   873  			verifyWaiting{},
   874  			verifyCharm{revision: 1},
   875  
   876  			resolveError{state.ResolvedNoHooks},
   877  			waitUnitAgent{
   878  				status: status.Idle,
   879  				charm:  1,
   880  			},
   881  			waitHooks{"config-changed"},
   882  			verifyRunning{},
   883  		),
   884  	})
   885  }
   886  
   887  func (s *UniterSuite) TestUniterUpgradeConflicts(c *gc.C) {
   888  	coretesting.SkipIfPPC64EL(c, "lp:1448308")
   889  	//TODO(bogdanteleaga): Fix this on windows
   890  	if runtime.GOOS == "windows" {
   891  		c.Skip("bug 1403084: currently does not work on windows")
   892  	}
   893  	s.runUniterTests(c, []uniterTest{
   894  		// Upgrade scenarios - handling conflicts.
   895  		ut(
   896  			"upgrade: resolving doesn't help until underlying problem is fixed",
   897  			startUpgradeError{},
   898  			resolveError{state.ResolvedNoHooks},
   899  			verifyWaitingUpgradeError{revision: 1},
   900  			fixUpgradeError{},
   901  			resolveError{state.ResolvedNoHooks},
   902  			waitHooks{"upgrade-charm", "config-changed"},
   903  			waitUnitAgent{
   904  				status: status.Idle,
   905  				charm:  1,
   906  			},
   907  			waitUnitAgent{
   908  				statusGetter: unitStatusGetter,
   909  				status:       status.Unknown,
   910  				charm:        1,
   911  			},
   912  			verifyCharm{revision: 1},
   913  		), ut(
   914  			`upgrade: forced upgrade does work without explicit resolution if underlying problem was fixed`,
   915  			startUpgradeError{},
   916  			resolveError{state.ResolvedNoHooks},
   917  			verifyWaitingUpgradeError{revision: 1},
   918  			fixUpgradeError{},
   919  			createCharm{revision: 2},
   920  			serveCharm{},
   921  			upgradeCharm{revision: 2, forced: true},
   922  			waitHooks{"upgrade-charm", "config-changed"},
   923  			waitUnitAgent{
   924  				status: status.Idle,
   925  				charm:  2,
   926  			},
   927  			waitUnitAgent{
   928  				statusGetter: unitStatusGetter,
   929  				status:       status.Unknown,
   930  				charm:        2,
   931  			},
   932  			verifyCharm{revision: 2},
   933  		), ut(
   934  			"upgrade conflict unit dying",
   935  			startUpgradeError{},
   936  			unitDying,
   937  			verifyWaitingUpgradeError{revision: 1},
   938  			fixUpgradeError{},
   939  			resolveError{state.ResolvedNoHooks},
   940  			waitHooks{"upgrade-charm", "config-changed", "leader-settings-changed", "stop"},
   941  			waitUniterDead{},
   942  		), ut(
   943  			"upgrade conflict unit dead",
   944  			startUpgradeError{},
   945  			unitDead,
   946  			waitUniterDead{},
   947  			waitHooks{},
   948  			fixUpgradeError{},
   949  		),
   950  	})
   951  }
   952  
   953  func (s *UniterSuite) TestUniterRelations(c *gc.C) {
   954  	waitDyingHooks := custom{func(c *gc.C, ctx *context) {
   955  		// There is no ordering relationship between relation hooks and
   956  		// leader-settings-changed hooks; and while we're dying we may
   957  		// never get to leader-settings-changed before it's time to run
   958  		// the stop (as we might not react to a config change in time).
   959  		// It's actually clearer to just list the possible orders:
   960  		possibles := [][]string{{
   961  			"leader-settings-changed",
   962  			"db-relation-departed mysql/0 db:0",
   963  			"db-relation-broken db:0",
   964  			"stop",
   965  		}, {
   966  			"db-relation-departed mysql/0 db:0",
   967  			"leader-settings-changed",
   968  			"db-relation-broken db:0",
   969  			"stop",
   970  		}, {
   971  			"db-relation-departed mysql/0 db:0",
   972  			"db-relation-broken db:0",
   973  			"leader-settings-changed",
   974  			"stop",
   975  		}, {
   976  			"db-relation-departed mysql/0 db:0",
   977  			"db-relation-broken db:0",
   978  			"stop",
   979  		}}
   980  		unchecked := ctx.hooksCompleted[len(ctx.hooks):]
   981  		for _, possible := range possibles {
   982  			if ok, _ := jc.DeepEqual(unchecked, possible); ok {
   983  				return
   984  			}
   985  		}
   986  		c.Fatalf("unexpected hooks: %v", unchecked)
   987  	}}
   988  	s.runUniterTests(c, []uniterTest{
   989  		// Relations.
   990  		ut(
   991  			"simple joined/changed/departed",
   992  			quickStartRelation{},
   993  			addRelationUnit{},
   994  			waitHooks{
   995  				"db-relation-joined mysql/1 db:0",
   996  				"db-relation-changed mysql/1 db:0",
   997  			},
   998  			changeRelationUnit{"mysql/0"},
   999  			waitHooks{"db-relation-changed mysql/0 db:0"},
  1000  			removeRelationUnit{"mysql/1"},
  1001  			waitHooks{"db-relation-departed mysql/1 db:0"},
  1002  			verifyRunning{},
  1003  		), ut(
  1004  			"relation becomes dying; unit is not last remaining member",
  1005  			quickStartRelation{},
  1006  			relationDying,
  1007  			waitHooks{
  1008  				"db-relation-departed mysql/0 db:0",
  1009  				"db-relation-broken db:0",
  1010  			},
  1011  			verifyRunning{},
  1012  			relationState{life: state.Dying},
  1013  			removeRelationUnit{"mysql/0"},
  1014  			verifyRunning{},
  1015  			relationState{removed: true},
  1016  			verifyRunning{},
  1017  		), ut(
  1018  			"relation becomes dying; unit is last remaining member",
  1019  			quickStartRelation{},
  1020  			removeRelationUnit{"mysql/0"},
  1021  			waitHooks{"db-relation-departed mysql/0 db:0"},
  1022  			relationDying,
  1023  			waitHooks{"db-relation-broken db:0"},
  1024  			verifyRunning{},
  1025  			relationState{removed: true},
  1026  			verifyRunning{},
  1027  		), ut(
  1028  			"unit becomes dying while in a relation",
  1029  			quickStartRelation{},
  1030  			unitDying,
  1031  			waitUniterDead{},
  1032  			waitDyingHooks,
  1033  			relationState{life: state.Alive},
  1034  			removeRelationUnit{"mysql/0"},
  1035  			relationState{life: state.Alive},
  1036  		), ut(
  1037  			"unit becomes dead while in a relation",
  1038  			quickStartRelation{},
  1039  			unitDead,
  1040  			waitUniterDead{},
  1041  			waitHooks{},
  1042  			// TODO BUG(?): the unit doesn't leave the scope, leaving the relation
  1043  			// unkillable without direct intervention. I'm pretty sure it's not a
  1044  			// uniter bug -- it should be the responsibility of `juju remove-unit
  1045  			// --force` to cause the unit to leave any relation scopes it may be
  1046  			// in -- but it's worth noting here all the same.
  1047  		), ut(
  1048  			"unknown local relation dir is removed",
  1049  			quickStartRelation{},
  1050  			stopUniter{},
  1051  			custom{func(c *gc.C, ctx *context) {
  1052  				ft.Dir{"state/relations/90210", 0755}.Create(c, ctx.path)
  1053  			}},
  1054  			startUniter{},
  1055  			// We need some synchronisation point here to ensure that the uniter
  1056  			// has entered the correct place in the resolving loop. Now that we are
  1057  			// no longer always executing config-changed, we poke the config just so
  1058  			// we can get the event to give us the synchronisation point.
  1059  			changeConfig{"blog-title": "Goodness Gracious Me"},
  1060  			waitHooks{"config-changed"},
  1061  			custom{func(c *gc.C, ctx *context) {
  1062  				ft.Removed{"state/relations/90210"}.Check(c, ctx.path)
  1063  			}},
  1064  		), ut(
  1065  			"all relations are available to config-changed on bounce, even if state dir is missing",
  1066  			createCharm{
  1067  				customize: func(c *gc.C, ctx *context, path string) {
  1068  					script := uniterRelationsCustomizeScript
  1069  					appendHook(c, path, "config-changed", script)
  1070  				},
  1071  			},
  1072  			serveCharm{},
  1073  			createUniter{},
  1074  			waitUnitAgent{
  1075  				status: status.Idle,
  1076  			},
  1077  			waitUnitAgent{
  1078  				statusGetter: unitStatusGetter,
  1079  				status:       status.Unknown,
  1080  			},
  1081  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1082  			addRelation{waitJoin: true},
  1083  			stopUniter{},
  1084  			custom{func(c *gc.C, ctx *context) {
  1085  				// Check the state dir was created, and remove it.
  1086  				path := fmt.Sprintf("state/relations/%d", ctx.relation.Id())
  1087  				ft.Dir{path, 0755}.Check(c, ctx.path)
  1088  				ft.Removed{path}.Create(c, ctx.path)
  1089  
  1090  				// Check that config-changed didn't record any relations, because
  1091  				// they shouldn't been available until after the start hook.
  1092  				ft.File{"charm/relations.out", "", 0644}.Check(c, ctx.path)
  1093  			}},
  1094  			// Now that starting the uniter doesn't always fire
  1095  			// config-changed we need to tweak the config to trigger
  1096  			// it.
  1097  			changeConfig{"blog-title": "Goodness Gracious Me"},
  1098  			startUniter{},
  1099  			waitHooks{"config-changed"},
  1100  			custom{func(c *gc.C, ctx *context) {
  1101  				// Check the state dir was recreated.
  1102  				path := fmt.Sprintf("state/relations/%d", ctx.relation.Id())
  1103  				ft.Dir{path, 0755}.Check(c, ctx.path)
  1104  
  1105  				// Check that config-changed did record the joined relations.
  1106  				data := fmt.Sprintf("db:%d\n", ctx.relation.Id())
  1107  				ft.File{"charm/relations.out", data, 0644}.Check(c, ctx.path)
  1108  			}},
  1109  		),
  1110  	})
  1111  }
  1112  
  1113  func (s *UniterSuite) TestUniterRelationErrors(c *gc.C) {
  1114  	s.runUniterTests(c, []uniterTest{
  1115  		ut(
  1116  			"hook error during join of a relation",
  1117  			startupRelationError{"db-relation-joined"},
  1118  			waitUnitAgent{
  1119  				statusGetter: unitStatusGetter,
  1120  				status:       status.Error,
  1121  				info:         `hook failed: "db-relation-joined"`,
  1122  				data: map[string]interface{}{
  1123  					"hook":        "db-relation-joined",
  1124  					"relation-id": 0,
  1125  					"remote-unit": "mysql/0",
  1126  				},
  1127  			},
  1128  		), ut(
  1129  			"hook error during change of a relation",
  1130  			startupRelationError{"db-relation-changed"},
  1131  			waitUnitAgent{
  1132  				statusGetter: unitStatusGetter,
  1133  				status:       status.Error,
  1134  				info:         `hook failed: "db-relation-changed"`,
  1135  				data: map[string]interface{}{
  1136  					"hook":        "db-relation-changed",
  1137  					"relation-id": 0,
  1138  					"remote-unit": "mysql/0",
  1139  				},
  1140  			},
  1141  		), ut(
  1142  			"hook error after a unit departed",
  1143  			startupRelationError{"db-relation-departed"},
  1144  			waitHooks{"db-relation-joined mysql/0 db:0", "db-relation-changed mysql/0 db:0"},
  1145  			removeRelationUnit{"mysql/0"},
  1146  			waitUnitAgent{
  1147  				statusGetter: unitStatusGetter,
  1148  				status:       status.Error,
  1149  				info:         `hook failed: "db-relation-departed"`,
  1150  				data: map[string]interface{}{
  1151  					"hook":        "db-relation-departed",
  1152  					"relation-id": 0,
  1153  					"remote-unit": "mysql/0",
  1154  				},
  1155  			},
  1156  		),
  1157  		ut(
  1158  			"hook error after a relation died",
  1159  			startupRelationError{"db-relation-broken"},
  1160  			waitHooks{"db-relation-joined mysql/0 db:0", "db-relation-changed mysql/0 db:0"},
  1161  			relationDying,
  1162  			waitUnitAgent{
  1163  				statusGetter: unitStatusGetter,
  1164  				status:       status.Error,
  1165  				info:         `hook failed: "db-relation-broken"`,
  1166  				data: map[string]interface{}{
  1167  					"hook":        "db-relation-broken",
  1168  					"relation-id": 0,
  1169  				},
  1170  			},
  1171  		),
  1172  	})
  1173  }
  1174  
  1175  func (s *UniterSuite) TestActionEvents(c *gc.C) {
  1176  	s.runUniterTests(c, []uniterTest{
  1177  		ut(
  1178  			"simple action event: defined in actions.yaml, no args",
  1179  			createCharm{
  1180  				customize: func(c *gc.C, ctx *context, path string) {
  1181  					ctx.writeAction(c, path, "action-log")
  1182  					ctx.writeActionsYaml(c, path, "action-log")
  1183  				},
  1184  			},
  1185  			serveCharm{},
  1186  			ensureStateWorker{},
  1187  			createApplicationAndUnit{},
  1188  			startUniter{},
  1189  			waitAddresses{},
  1190  			waitUnitAgent{status: status.Idle},
  1191  			waitUnitAgent{
  1192  				statusGetter: unitStatusGetter,
  1193  				status:       status.Unknown,
  1194  			},
  1195  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1196  			verifyCharm{},
  1197  			addAction{"action-log", nil},
  1198  			waitActionResults{[]actionResult{{
  1199  				name:    "action-log",
  1200  				results: map[string]interface{}{},
  1201  				status:  params.ActionCompleted,
  1202  			}}},
  1203  			waitUnitAgent{status: status.Idle},
  1204  			waitUnitAgent{
  1205  				statusGetter: unitStatusGetter,
  1206  				status:       status.Unknown,
  1207  			},
  1208  		), ut(
  1209  			"action-fail causes the action to fail with a message",
  1210  			createCharm{
  1211  				customize: func(c *gc.C, ctx *context, path string) {
  1212  					ctx.writeAction(c, path, "action-log-fail")
  1213  					ctx.writeActionsYaml(c, path, "action-log-fail")
  1214  				},
  1215  			},
  1216  			serveCharm{},
  1217  			ensureStateWorker{},
  1218  			createApplicationAndUnit{},
  1219  			startUniter{},
  1220  			waitAddresses{},
  1221  			waitUnitAgent{status: status.Idle},
  1222  			waitUnitAgent{
  1223  				statusGetter: unitStatusGetter,
  1224  				status:       status.Unknown,
  1225  			},
  1226  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1227  			verifyCharm{},
  1228  			addAction{"action-log-fail", nil},
  1229  			waitActionResults{[]actionResult{{
  1230  				name: "action-log-fail",
  1231  				results: map[string]interface{}{
  1232  					"foo": "still works",
  1233  				},
  1234  				message: "I'm afraid I can't let you do that, Dave.",
  1235  				status:  params.ActionFailed,
  1236  			}}},
  1237  			waitUnitAgent{status: status.Idle}, waitUnitAgent{
  1238  				statusGetter: unitStatusGetter,
  1239  				status:       status.Unknown,
  1240  			},
  1241  		), ut(
  1242  			"action-fail with the wrong arguments fails but is not an error",
  1243  			createCharm{
  1244  				customize: func(c *gc.C, ctx *context, path string) {
  1245  					ctx.writeAction(c, path, "action-log-fail-error")
  1246  					ctx.writeActionsYaml(c, path, "action-log-fail-error")
  1247  				},
  1248  			},
  1249  			serveCharm{},
  1250  			ensureStateWorker{},
  1251  			createApplicationAndUnit{},
  1252  			startUniter{},
  1253  			waitAddresses{},
  1254  			waitUnitAgent{status: status.Idle},
  1255  			waitUnitAgent{
  1256  				statusGetter: unitStatusGetter,
  1257  				status:       status.Unknown,
  1258  			},
  1259  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1260  			verifyCharm{},
  1261  			addAction{"action-log-fail-error", nil},
  1262  			waitActionResults{[]actionResult{{
  1263  				name: "action-log-fail-error",
  1264  				results: map[string]interface{}{
  1265  					"foo": "still works",
  1266  				},
  1267  				message: "A real message",
  1268  				status:  params.ActionFailed,
  1269  			}}},
  1270  			waitUnitAgent{status: status.Idle},
  1271  			waitUnitAgent{
  1272  				statusGetter: unitStatusGetter,
  1273  				status:       status.Unknown,
  1274  			},
  1275  		), ut(
  1276  			"actions with correct params passed are not an error",
  1277  			createCharm{
  1278  				customize: func(c *gc.C, ctx *context, path string) {
  1279  					ctx.writeAction(c, path, "snapshot")
  1280  					ctx.writeActionsYaml(c, path, "snapshot")
  1281  				},
  1282  			},
  1283  			serveCharm{},
  1284  			ensureStateWorker{},
  1285  			createApplicationAndUnit{},
  1286  			startUniter{},
  1287  			waitAddresses{},
  1288  			waitUnitAgent{status: status.Idle},
  1289  			waitUnitAgent{
  1290  				statusGetter: unitStatusGetter,
  1291  				status:       status.Unknown,
  1292  			},
  1293  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1294  			verifyCharm{},
  1295  			addAction{
  1296  				name:   "snapshot",
  1297  				params: map[string]interface{}{"outfile": "foo.bar"},
  1298  			},
  1299  			waitActionResults{[]actionResult{{
  1300  				name: "snapshot",
  1301  				results: map[string]interface{}{
  1302  					"outfile": map[string]interface{}{
  1303  						"name": "snapshot-01.tar",
  1304  						"size": map[string]interface{}{
  1305  							"magnitude": "10.3",
  1306  							"units":     "GB",
  1307  						},
  1308  					},
  1309  					"completion": "yes",
  1310  				},
  1311  				status: params.ActionCompleted,
  1312  			}}},
  1313  			waitUnitAgent{status: status.Idle},
  1314  			waitUnitAgent{
  1315  				statusGetter: unitStatusGetter,
  1316  				status:       status.Unknown,
  1317  			},
  1318  		), ut(
  1319  			"actions with incorrect params passed are not an error but fail",
  1320  			createCharm{
  1321  				customize: func(c *gc.C, ctx *context, path string) {
  1322  					ctx.writeAction(c, path, "snapshot")
  1323  					ctx.writeActionsYaml(c, path, "snapshot")
  1324  				},
  1325  			},
  1326  			serveCharm{},
  1327  			ensureStateWorker{},
  1328  			createApplicationAndUnit{},
  1329  			startUniter{},
  1330  			waitAddresses{},
  1331  			waitUnitAgent{status: status.Idle},
  1332  			waitUnitAgent{
  1333  				statusGetter: unitStatusGetter,
  1334  				status:       status.Unknown,
  1335  			},
  1336  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1337  			verifyCharm{},
  1338  			addAction{
  1339  				name:   "snapshot",
  1340  				params: map[string]interface{}{"outfile": 2},
  1341  			},
  1342  			waitActionResults{[]actionResult{{
  1343  				name:    "snapshot",
  1344  				results: map[string]interface{}{},
  1345  				status:  params.ActionFailed,
  1346  				message: `cannot run "snapshot" action: validation failed: (root).outfile : must be of type string, given 2`,
  1347  			}}},
  1348  			waitUnitAgent{status: status.Idle},
  1349  			waitUnitAgent{
  1350  				statusGetter: unitStatusGetter,
  1351  				status:       status.Unknown,
  1352  			},
  1353  		), ut(
  1354  			"actions not defined in actions.yaml fail without causing a uniter error",
  1355  			createCharm{
  1356  				customize: func(c *gc.C, ctx *context, path string) {
  1357  					ctx.writeAction(c, path, "snapshot")
  1358  				},
  1359  			},
  1360  			serveCharm{},
  1361  			ensureStateWorker{},
  1362  			createApplicationAndUnit{},
  1363  			startUniter{},
  1364  			waitAddresses{},
  1365  			waitUnitAgent{status: status.Idle},
  1366  			waitUnitAgent{
  1367  				statusGetter: unitStatusGetter,
  1368  				status:       status.Unknown,
  1369  			},
  1370  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1371  			verifyCharm{},
  1372  			addAction{"snapshot", map[string]interface{}{"outfile": "foo.bar"}},
  1373  			waitActionResults{[]actionResult{{
  1374  				name:    "snapshot",
  1375  				results: map[string]interface{}{},
  1376  				status:  params.ActionFailed,
  1377  				message: `cannot run "snapshot" action: not defined`,
  1378  			}}},
  1379  			waitUnitAgent{status: status.Idle},
  1380  			waitUnitAgent{
  1381  				statusGetter: unitStatusGetter,
  1382  				status:       status.Unknown,
  1383  			},
  1384  		), ut(
  1385  			"pending actions get consumed",
  1386  			createCharm{
  1387  				customize: func(c *gc.C, ctx *context, path string) {
  1388  					ctx.writeAction(c, path, "action-log")
  1389  					ctx.writeActionsYaml(c, path, "action-log")
  1390  				},
  1391  			},
  1392  			serveCharm{},
  1393  			ensureStateWorker{},
  1394  			createApplicationAndUnit{},
  1395  			addAction{"action-log", nil},
  1396  			addAction{"action-log", nil},
  1397  			addAction{"action-log", nil},
  1398  			startUniter{},
  1399  			waitAddresses{},
  1400  			waitUnitAgent{status: status.Idle},
  1401  			waitUnitAgent{
  1402  				statusGetter: unitStatusGetter,
  1403  				status:       status.Unknown,
  1404  			},
  1405  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1406  			verifyCharm{},
  1407  			waitActionResults{[]actionResult{{
  1408  				name:    "action-log",
  1409  				results: map[string]interface{}{},
  1410  				status:  params.ActionCompleted,
  1411  			}, {
  1412  				name:    "action-log",
  1413  				results: map[string]interface{}{},
  1414  				status:  params.ActionCompleted,
  1415  			}, {
  1416  				name:    "action-log",
  1417  				results: map[string]interface{}{},
  1418  				status:  params.ActionCompleted,
  1419  			}}},
  1420  			waitUnitAgent{status: status.Idle},
  1421  			waitUnitAgent{
  1422  				statusGetter: unitStatusGetter,
  1423  				status:       status.Unknown,
  1424  			},
  1425  		), ut(
  1426  			"actions not implemented fail but are not errors",
  1427  			createCharm{
  1428  				customize: func(c *gc.C, ctx *context, path string) {
  1429  					ctx.writeActionsYaml(c, path, "action-log")
  1430  				},
  1431  			},
  1432  			serveCharm{},
  1433  			ensureStateWorker{},
  1434  			createApplicationAndUnit{},
  1435  			startUniter{},
  1436  			waitAddresses{},
  1437  			waitUnitAgent{status: status.Idle},
  1438  			waitUnitAgent{
  1439  				statusGetter: unitStatusGetter,
  1440  				status:       status.Unknown,
  1441  			},
  1442  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1443  			verifyCharm{},
  1444  			addAction{"action-log", nil},
  1445  			waitActionResults{[]actionResult{{
  1446  				name:    "action-log",
  1447  				results: map[string]interface{}{},
  1448  				status:  params.ActionFailed,
  1449  				message: `action not implemented on unit "u/0"`,
  1450  			}}},
  1451  			waitUnitAgent{status: status.Idle},
  1452  			waitUnitAgent{
  1453  				statusGetter: unitStatusGetter,
  1454  				status:       status.Unknown,
  1455  			},
  1456  		), ut(
  1457  			"actions may run from ModeHookError, but do not clear the error",
  1458  			startupErrorWithCustomCharm{
  1459  				badHook: "start",
  1460  				customize: func(c *gc.C, ctx *context, path string) {
  1461  					ctx.writeAction(c, path, "action-log")
  1462  					ctx.writeActionsYaml(c, path, "action-log")
  1463  				},
  1464  			},
  1465  			addAction{"action-log", nil},
  1466  			waitUnitAgent{
  1467  				statusGetter: unitStatusGetter,
  1468  				status:       status.Error,
  1469  				info:         `hook failed: "start"`,
  1470  				data:         map[string]interface{}{"hook": "start"},
  1471  			},
  1472  			waitActionResults{[]actionResult{{
  1473  				name:    "action-log",
  1474  				results: map[string]interface{}{},
  1475  				status:  params.ActionCompleted,
  1476  			}}},
  1477  			waitUnitAgent{
  1478  				statusGetter: unitStatusGetter,
  1479  				status:       status.Error,
  1480  				info:         `hook failed: "start"`,
  1481  				data:         map[string]interface{}{"hook": "start"},
  1482  			},
  1483  			verifyWaiting{},
  1484  			resolveError{state.ResolvedNoHooks},
  1485  			waitUnitAgent{status: status.Idle},
  1486  			waitUnitAgent{
  1487  				statusGetter: unitStatusGetter,
  1488  				status:       status.Maintenance,
  1489  				info:         "installing charm software",
  1490  			},
  1491  		),
  1492  	})
  1493  }
  1494  
  1495  func (s *UniterSuite) TestUniterSubordinates(c *gc.C) {
  1496  	s.runUniterTests(c, []uniterTest{
  1497  		// Subordinates.
  1498  		ut(
  1499  			"unit becomes dying while subordinates exist",
  1500  			quickStart{},
  1501  			addSubordinateRelation{"juju-info"},
  1502  			waitSubordinateExists{"logging/0"},
  1503  			unitDying,
  1504  			waitSubordinateDying{},
  1505  			waitHooks{"leader-settings-changed", "stop"},
  1506  			verifyWaiting{},
  1507  			removeSubordinate{},
  1508  			waitUniterDead{},
  1509  		), ut(
  1510  			"new subordinate becomes necessary while old one is dying",
  1511  			quickStart{},
  1512  			addSubordinateRelation{"juju-info"},
  1513  			waitSubordinateExists{"logging/0"},
  1514  			removeSubordinateRelation{"juju-info"},
  1515  			// The subordinate Uniter would usually set Dying in this situation.
  1516  			subordinateDying,
  1517  			addSubordinateRelation{"logging-dir"},
  1518  			verifyRunning{},
  1519  			removeSubordinate{},
  1520  			waitSubordinateExists{"logging/1"},
  1521  		),
  1522  	})
  1523  }
  1524  
  1525  func (s *UniterSuite) TestSubordinateDying(c *gc.C) {
  1526  	// Create a test context for later use.
  1527  	ctx := &context{
  1528  		s:                      s,
  1529  		st:                     s.State,
  1530  		path:                   filepath.Join(s.dataDir, "agents", "unit-u-0"),
  1531  		dataDir:                s.dataDir,
  1532  		charms:                 make(map[string][]byte),
  1533  		leaseManager:           s.LeaseManager,
  1534  		updateStatusHookTicker: s.updateStatusHookTicker,
  1535  		charmDirGuard:          &mockCharmDirGuard{},
  1536  	}
  1537  
  1538  	addControllerMachine(c, ctx.st)
  1539  
  1540  	// Create the subordinate application.
  1541  	dir := testcharms.Repo.ClonedDir(c.MkDir(), "logging")
  1542  	curl, err := corecharm.ParseURL("cs:quantal/logging")
  1543  	c.Assert(err, jc.ErrorIsNil)
  1544  	curl = curl.WithRevision(dir.Revision())
  1545  	step(c, ctx, addCharm{dir, curl})
  1546  	ctx.application = s.AddTestingApplication(c, "u", ctx.sch)
  1547  
  1548  	// Create the principal application and add a relation.
  1549  	wps := s.AddTestingApplication(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
  1550  	wpu, err := wps.AddUnit(state.AddUnitParams{})
  1551  	c.Assert(err, jc.ErrorIsNil)
  1552  	eps, err := s.State.InferEndpoints("wordpress", "u")
  1553  	c.Assert(err, jc.ErrorIsNil)
  1554  	rel, err := s.State.AddRelation(eps...)
  1555  	c.Assert(err, jc.ErrorIsNil)
  1556  	assertAssignUnit(c, s.State, wpu)
  1557  
  1558  	// Create the subordinate unit by entering scope as the principal.
  1559  	wpru, err := rel.Unit(wpu)
  1560  	c.Assert(err, jc.ErrorIsNil)
  1561  	err = wpru.EnterScope(nil)
  1562  	c.Assert(err, jc.ErrorIsNil)
  1563  	ctx.unit, err = s.State.Unit("u/0")
  1564  	c.Assert(err, jc.ErrorIsNil)
  1565  	ctx.apiLogin(c)
  1566  
  1567  	// Run the actual test.
  1568  	ctx.run(c, []stepper{
  1569  		serveCharm{},
  1570  		startUniter{},
  1571  		waitAddresses{},
  1572  		custom{func(c *gc.C, ctx *context) {
  1573  			c.Assert(rel.Destroy(), gc.IsNil)
  1574  		}},
  1575  		waitUniterDead{},
  1576  	})
  1577  }
  1578  
  1579  func (s *UniterSuite) TestRebootDisabledInActions(c *gc.C) {
  1580  	s.runUniterTests(c, []uniterTest{
  1581  		ut(
  1582  			"test that juju-reboot disabled in actions",
  1583  			createCharm{
  1584  				customize: func(c *gc.C, ctx *context, path string) {
  1585  					ctx.writeAction(c, path, "action-reboot")
  1586  					ctx.writeActionsYaml(c, path, "action-reboot")
  1587  				},
  1588  			},
  1589  			serveCharm{},
  1590  			ensureStateWorker{},
  1591  			createApplicationAndUnit{},
  1592  			addAction{"action-reboot", nil},
  1593  			startUniter{},
  1594  			waitAddresses{},
  1595  			waitUnitAgent{
  1596  				status: status.Idle,
  1597  			},
  1598  			waitUnitAgent{
  1599  				statusGetter: unitStatusGetter,
  1600  				status:       status.Unknown,
  1601  			},
  1602  			waitActionResults{[]actionResult{{
  1603  				name: "action-reboot",
  1604  				results: map[string]interface{}{
  1605  					"reboot-delayed": "good",
  1606  					"reboot-now":     "good",
  1607  				},
  1608  				status: params.ActionCompleted,
  1609  			}}},
  1610  		)})
  1611  }
  1612  
  1613  func (s *UniterSuite) TestRebootFinishesHook(c *gc.C) {
  1614  	s.runUniterTests(c, []uniterTest{
  1615  		ut(
  1616  			"test that juju-reboot finishes hook, and reboots",
  1617  			createCharm{
  1618  				customize: func(c *gc.C, ctx *context, path string) {
  1619  					hpath := filepath.Join(path, "hooks", "install")
  1620  					ctx.writeExplicitHook(c, hpath, rebootHook)
  1621  				},
  1622  			},
  1623  			serveCharm{},
  1624  			ensureStateWorker{},
  1625  			createApplicationAndUnit{},
  1626  			startUniter{},
  1627  			waitAddresses{},
  1628  			waitUniterDead{err: "machine needs to reboot"},
  1629  			waitHooks{"install"},
  1630  			startUniter{},
  1631  			waitUnitAgent{
  1632  				status: status.Idle,
  1633  			},
  1634  			waitUnitAgent{
  1635  				statusGetter: unitStatusGetter,
  1636  				status:       status.Unknown,
  1637  			},
  1638  			waitHooks{"leader-elected", "config-changed", "start"},
  1639  		)})
  1640  }
  1641  
  1642  func (s *UniterSuite) TestRebootNowKillsHook(c *gc.C) {
  1643  	s.runUniterTests(c, []uniterTest{
  1644  		ut(
  1645  			"test that juju-reboot --now kills hook and exits",
  1646  			createCharm{
  1647  				customize: func(c *gc.C, ctx *context, path string) {
  1648  					hpath := filepath.Join(path, "hooks", "install")
  1649  					ctx.writeExplicitHook(c, hpath, rebootNowHook)
  1650  				},
  1651  			},
  1652  			serveCharm{},
  1653  			ensureStateWorker{},
  1654  			createApplicationAndUnit{},
  1655  			startUniter{},
  1656  			waitAddresses{},
  1657  			waitUniterDead{err: "machine needs to reboot"},
  1658  			waitHooks{"install"},
  1659  			startUniter{},
  1660  			waitUnitAgent{
  1661  				status: status.Idle,
  1662  			},
  1663  			waitUnitAgent{
  1664  				statusGetter: unitStatusGetter,
  1665  				status:       status.Unknown,
  1666  			},
  1667  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1668  		)})
  1669  }
  1670  
  1671  func (s *UniterSuite) TestRebootDisabledOnHookError(c *gc.C) {
  1672  	s.runUniterTests(c, []uniterTest{
  1673  		ut(
  1674  			"test juju-reboot will not happen if hook errors out",
  1675  			createCharm{
  1676  				customize: func(c *gc.C, ctx *context, path string) {
  1677  					hpath := filepath.Join(path, "hooks", "install")
  1678  					ctx.writeExplicitHook(c, hpath, badRebootHook)
  1679  				},
  1680  			},
  1681  			serveCharm{},
  1682  			ensureStateWorker{},
  1683  			createApplicationAndUnit{},
  1684  			startUniter{},
  1685  			waitAddresses{},
  1686  			waitUnitAgent{
  1687  				statusGetter: unitStatusGetter,
  1688  				status:       status.Error,
  1689  				info:         fmt.Sprintf(`hook failed: "install"`),
  1690  			},
  1691  		),
  1692  	})
  1693  }
  1694  
  1695  func (s *UniterSuite) TestJujuRunExecutionSerialized(c *gc.C) {
  1696  	s.runUniterTests(c, []uniterTest{
  1697  		ut(
  1698  			"hook failed status should stay around after juju run",
  1699  			createCharm{badHooks: []string{"config-changed"}},
  1700  			serveCharm{},
  1701  			createUniter{},
  1702  			waitUnitAgent{
  1703  				statusGetter: unitStatusGetter,
  1704  				status:       status.Error,
  1705  				info:         `hook failed: "config-changed"`,
  1706  				data: map[string]interface{}{
  1707  					"hook": "config-changed",
  1708  				},
  1709  			},
  1710  			runCommands{"exit 0"},
  1711  			waitUnitAgent{
  1712  				statusGetter: unitStatusGetter,
  1713  				status:       status.Error,
  1714  				info:         `hook failed: "config-changed"`,
  1715  				data: map[string]interface{}{
  1716  					"hook": "config-changed",
  1717  				},
  1718  			},
  1719  		)})
  1720  }
  1721  
  1722  func (s *UniterSuite) TestRebootFromJujuRun(c *gc.C) {
  1723  	//TODO(bogdanteleaga): Fix this on windows
  1724  	if runtime.GOOS == "windows" {
  1725  		c.Skip("bug 1403084: currently does not work on windows")
  1726  	}
  1727  	s.runUniterTests(c, []uniterTest{
  1728  		ut(
  1729  			"test juju-reboot",
  1730  			quickStart{},
  1731  			runCommands{"juju-reboot"},
  1732  			waitUniterDead{err: "machine needs to reboot"},
  1733  			startUniter{},
  1734  			waitHooks{},
  1735  		), ut(
  1736  			"test juju-reboot with bad hook",
  1737  			startupError{"install"},
  1738  			runCommands{"juju-reboot"},
  1739  			waitUniterDead{err: "machine needs to reboot"},
  1740  			startUniter{},
  1741  			waitHooks{},
  1742  		), ut(
  1743  			"test juju-reboot --now",
  1744  			quickStart{},
  1745  			runCommands{"juju-reboot --now"},
  1746  			waitUniterDead{err: "machine needs to reboot"},
  1747  			startUniter{},
  1748  			waitHooks{},
  1749  		), ut(
  1750  			"test juju-reboot --now with bad hook",
  1751  			startupError{"install"},
  1752  			runCommands{"juju-reboot --now"},
  1753  			waitUniterDead{err: "machine needs to reboot"},
  1754  			startUniter{},
  1755  			waitHooks{},
  1756  		),
  1757  	})
  1758  }
  1759  
  1760  func (s *UniterSuite) TestLeadership(c *gc.C) {
  1761  	s.runUniterTests(c, []uniterTest{
  1762  		ut(
  1763  			"hook tools when leader",
  1764  			quickStart{},
  1765  			runCommands{"leader-set foo=bar baz=qux"},
  1766  			verifyLeaderSettings{"foo": "bar", "baz": "qux"},
  1767  		), ut(
  1768  			"hook tools when not leader",
  1769  			quickStart{minion: true},
  1770  			runCommands{leadershipScript},
  1771  		), ut(
  1772  			"leader-elected triggers when elected",
  1773  			quickStart{minion: true},
  1774  			forceLeader{},
  1775  			waitHooks{"leader-elected"},
  1776  		), ut(
  1777  			"leader-settings-changed triggers when leader settings change",
  1778  			quickStart{minion: true},
  1779  			setLeaderSettings{"ping": "pong"},
  1780  			waitHooks{"leader-settings-changed"},
  1781  		), ut(
  1782  			"leader-settings-changed triggers when bounced",
  1783  			quickStart{minion: true},
  1784  			verifyRunning{minion: true},
  1785  		), ut(
  1786  			"leader-settings-changed triggers when deposed (while stopped)",
  1787  			quickStart{},
  1788  			stopUniter{},
  1789  			forceMinion{},
  1790  			verifyRunning{minion: true},
  1791  		),
  1792  	})
  1793  }
  1794  
  1795  func (s *UniterSuite) TestLeadershipUnexpectedDepose(c *gc.C) {
  1796  	s.runUniterTests(c, []uniterTest{
  1797  		ut(
  1798  			// NOTE: this is a strange and ugly test, intended to detect what
  1799  			// *would* happen if the uniter suddenly failed to renew its lease;
  1800  			// it depends on an artificially shortened tracker refresh time to
  1801  			// run in a reasonable amount of time.
  1802  			"leader-settings-changed triggers when deposed (while running)",
  1803  			quickStart{},
  1804  			forceMinion{},
  1805  			waitHooks{"leader-settings-changed"},
  1806  		),
  1807  	})
  1808  }
  1809  
  1810  func (s *UniterSuite) TestStorage(c *gc.C) {
  1811  	// appendStorageMetadata customises the wordpress charm's metadata,
  1812  	// adding a "wp-content" filesystem store. We do it here rather
  1813  	// than in the charm itself to avoid modifying all of the other
  1814  	// scenarios.
  1815  	appendStorageMetadata := func(c *gc.C, ctx *context, path string) {
  1816  		f, err := os.OpenFile(filepath.Join(path, "metadata.yaml"), os.O_RDWR|os.O_APPEND, 0644)
  1817  		c.Assert(err, jc.ErrorIsNil)
  1818  		defer func() {
  1819  			err := f.Close()
  1820  			c.Assert(err, jc.ErrorIsNil)
  1821  		}()
  1822  		_, err = io.WriteString(f, `
  1823  storage:
  1824    wp-content:
  1825      type: filesystem
  1826      multiple:
  1827        range: 0-
  1828  `[1:])
  1829  		c.Assert(err, jc.ErrorIsNil)
  1830  	}
  1831  	storageConstraints := map[string]state.StorageConstraints{
  1832  		"wp-content": {Count: 1},
  1833  	}
  1834  	s.runUniterTests(c, []uniterTest{
  1835  		ut(
  1836  			"test that storage-attached is called",
  1837  			createCharm{customize: appendStorageMetadata},
  1838  			serveCharm{},
  1839  			ensureStateWorker{},
  1840  			createApplicationAndUnit{storage: storageConstraints},
  1841  			provisionStorage{},
  1842  			startUniter{},
  1843  			waitAddresses{},
  1844  			waitHooks{"wp-content-storage-attached"},
  1845  			waitHooks(startupHooks(false)),
  1846  		), ut(
  1847  			"test that storage-detaching is called before stop",
  1848  			createCharm{customize: appendStorageMetadata},
  1849  			serveCharm{},
  1850  			ensureStateWorker{},
  1851  			createApplicationAndUnit{storage: storageConstraints},
  1852  			provisionStorage{},
  1853  			startUniter{},
  1854  			waitAddresses{},
  1855  			waitHooks{"wp-content-storage-attached"},
  1856  			waitHooks(startupHooks(false)),
  1857  			unitDying,
  1858  			waitHooks{"leader-settings-changed"},
  1859  			// "stop" hook is not called until storage is detached
  1860  			waitHooks{"wp-content-storage-detaching", "stop"},
  1861  			verifyStorageDetached{},
  1862  			waitUniterDead{},
  1863  		), ut(
  1864  			"test that storage-detaching is called only if previously attached",
  1865  			createCharm{customize: appendStorageMetadata},
  1866  			serveCharm{},
  1867  			ensureStateWorker{},
  1868  			createApplicationAndUnit{storage: storageConstraints},
  1869  			// provision and destroy the storage before the uniter starts,
  1870  			// to ensure it never sees the storage as attached
  1871  			provisionStorage{},
  1872  			destroyStorageAttachment{},
  1873  			startUniter{},
  1874  			waitHooks(startupHooks(false)),
  1875  			unitDying,
  1876  			// storage-detaching is not called because it was never attached
  1877  			waitHooks{"leader-settings-changed", "stop"},
  1878  			verifyStorageDetached{},
  1879  			waitUniterDead{},
  1880  		), ut(
  1881  			"test that delay-provisioned storage does not block forever",
  1882  			createCharm{customize: appendStorageMetadata},
  1883  			serveCharm{},
  1884  			ensureStateWorker{},
  1885  			createApplicationAndUnit{storage: storageConstraints},
  1886  			startUniter{},
  1887  			// no hooks should be run, as storage isn't provisioned
  1888  			waitHooks{},
  1889  			provisionStorage{},
  1890  			waitHooks{"wp-content-storage-attached"},
  1891  			waitHooks(startupHooks(false)),
  1892  		), ut(
  1893  			"test that unprovisioned storage does not block unit termination",
  1894  			createCharm{customize: appendStorageMetadata},
  1895  			serveCharm{},
  1896  			ensureStateWorker{},
  1897  			createApplicationAndUnit{storage: storageConstraints},
  1898  			unitDying,
  1899  			startUniter{},
  1900  			// no hooks should be run, and unit agent should terminate
  1901  			waitHooks{},
  1902  			waitUniterDead{},
  1903  		),
  1904  		// TODO(axw) test that storage-attached is run for new
  1905  		// storage attachments before upgrade-charm is run. This
  1906  		// requires additions to state to add storage when a charm
  1907  		// is upgraded.
  1908  	})
  1909  }
  1910  
  1911  var mockExecutorErr = errors.New("some error occurred")
  1912  
  1913  type mockExecutor struct {
  1914  	operation.Executor
  1915  }
  1916  
  1917  func (m *mockExecutor) Run(op operation.Operation) error {
  1918  	// want to allow charm unpacking to occur
  1919  	if strings.HasPrefix(op.String(), "install") {
  1920  		return m.Executor.Run(op)
  1921  	}
  1922  	// but hooks should error
  1923  	return mockExecutorErr
  1924  }
  1925  
  1926  func (s *UniterSuite) TestOperationErrorReported(c *gc.C) {
  1927  	executorFunc := func(stateFilePath string, initialState operation.State, acquireLock func(string) (func(), error)) (operation.Executor, error) {
  1928  		e, err := operation.NewExecutor(stateFilePath, initialState, acquireLock)
  1929  		c.Assert(err, jc.ErrorIsNil)
  1930  		return &mockExecutor{e}, nil
  1931  	}
  1932  	s.runUniterTests(c, []uniterTest{
  1933  		ut(
  1934  			"error running operations are reported",
  1935  			createCharm{},
  1936  			serveCharm{},
  1937  			createUniter{executorFunc: executorFunc},
  1938  			waitUnitAgent{
  1939  				status: status.Failed,
  1940  				info:   "resolver loop error",
  1941  			},
  1942  			expectError{".*some error occurred.*"},
  1943  		),
  1944  	})
  1945  }
  1946  
  1947  func (s *UniterSuite) TestTranslateResolverError(c *gc.C) {
  1948  	executorFunc := func(stateFilePath string, initialState operation.State, acquireLock func(string) (func(), error)) (operation.Executor, error) {
  1949  		e, err := operation.NewExecutor(stateFilePath, initialState, acquireLock)
  1950  		c.Assert(err, jc.ErrorIsNil)
  1951  		return &mockExecutor{e}, nil
  1952  	}
  1953  	translateResolverErr := func(in error) error {
  1954  		c.Check(errors.Cause(in), gc.Equals, mockExecutorErr)
  1955  		return errors.New("some other error")
  1956  	}
  1957  	s.runUniterTests(c, []uniterTest{
  1958  		ut(
  1959  			"resolver errors are translated",
  1960  			createCharm{},
  1961  			serveCharm{},
  1962  			createUniter{
  1963  				executorFunc:         executorFunc,
  1964  				translateResolverErr: translateResolverErr,
  1965  			},
  1966  			waitUnitAgent{
  1967  				status: status.Failed,
  1968  				info:   "resolver loop error",
  1969  			},
  1970  			expectError{".*some other error.*"},
  1971  		),
  1972  	})
  1973  }