launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/state/megawatcher_internal_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"fmt"
     8  	"launchpad.net/errgo/errors"
     9  	"reflect"
    10  	"sort"
    11  	"time"
    12  
    13  	"labix.org/v2/mgo"
    14  	gc "launchpad.net/gocheck"
    15  
    16  	"launchpad.net/juju-core/charm"
    17  	"launchpad.net/juju-core/constraints"
    18  	"launchpad.net/juju-core/instance"
    19  	"launchpad.net/juju-core/state/api/params"
    20  	"launchpad.net/juju-core/state/multiwatcher"
    21  	"launchpad.net/juju-core/state/watcher"
    22  	"launchpad.net/juju-core/testing"
    23  	"launchpad.net/juju-core/testing/testbase"
    24  )
    25  
    26  var dottedConfig = `
    27  options:
    28    key.dotted: {default: My Key, description: Desc, type: string}
    29  `
    30  
    31  type storeManagerStateSuite struct {
    32  	testbase.LoggingSuite
    33  	testing.MgoSuite
    34  	State *State
    35  }
    36  
    37  func (s *storeManagerStateSuite) SetUpSuite(c *gc.C) {
    38  	s.LoggingSuite.SetUpSuite(c)
    39  	s.MgoSuite.SetUpSuite(c)
    40  }
    41  
    42  func (s *storeManagerStateSuite) TearDownSuite(c *gc.C) {
    43  	s.MgoSuite.TearDownSuite(c)
    44  	s.LoggingSuite.TearDownSuite(c)
    45  }
    46  
    47  func (s *storeManagerStateSuite) SetUpTest(c *gc.C) {
    48  	s.LoggingSuite.SetUpTest(c)
    49  	s.MgoSuite.SetUpTest(c)
    50  	s.State = TestingInitialize(c, nil)
    51  	s.State.AddUser("admin", "pass")
    52  }
    53  
    54  func (s *storeManagerStateSuite) TearDownTest(c *gc.C) {
    55  	s.State.Close()
    56  	s.MgoSuite.TearDownTest(c)
    57  	s.LoggingSuite.TearDownTest(c)
    58  }
    59  
    60  func (s *storeManagerStateSuite) Reset(c *gc.C) {
    61  	s.TearDownTest(c)
    62  	s.SetUpTest(c)
    63  }
    64  
    65  var _ = gc.Suite(&storeManagerStateSuite{})
    66  
    67  // setUpScenario adds some entities to the state so that
    68  // we can check that they all get pulled in by
    69  // allWatcherStateBacking.getAll.
    70  func (s *storeManagerStateSuite) setUpScenario(c *gc.C) (entities entityInfoSlice) {
    71  	add := func(e params.EntityInfo) {
    72  		entities = append(entities, e)
    73  	}
    74  	m, err := s.State.AddMachine("quantal", JobManageEnviron)
    75  	c.Assert(err, gc.IsNil)
    76  	c.Assert(m.Tag(), gc.Equals, "machine-0")
    77  	err = m.SetProvisioned(instance.Id("i-"+m.Tag()), "fake_nonce", nil)
    78  	c.Assert(err, gc.IsNil)
    79  	add(&params.MachineInfo{
    80  		Id:         "0",
    81  		InstanceId: "i-machine-0",
    82  		Status:     params.StatusPending,
    83  	})
    84  
    85  	wordpress := AddTestingService(c, s.State, "wordpress", AddTestingCharm(c, s.State, "wordpress"))
    86  	err = wordpress.SetExposed()
    87  	c.Assert(err, gc.IsNil)
    88  	err = wordpress.SetMinUnits(3)
    89  	c.Assert(err, gc.IsNil)
    90  	err = wordpress.SetConstraints(constraints.MustParse("mem=100M"))
    91  	c.Assert(err, gc.IsNil)
    92  	setServiceConfigAttr(c, wordpress, "blog-title", "boring")
    93  	add(&params.ServiceInfo{
    94  		Name:        "wordpress",
    95  		Exposed:     true,
    96  		CharmURL:    serviceCharmURL(wordpress).String(),
    97  		OwnerTag:    "user-admin",
    98  		Life:        params.Life(Alive.String()),
    99  		MinUnits:    3,
   100  		Constraints: constraints.MustParse("mem=100M"),
   101  		Config:      charm.Settings{"blog-title": "boring"},
   102  	})
   103  	pairs := map[string]string{"x": "12", "y": "99"}
   104  	err = wordpress.SetAnnotations(pairs)
   105  	c.Assert(err, gc.IsNil)
   106  	add(&params.AnnotationInfo{
   107  		Tag:         "service-wordpress",
   108  		Annotations: pairs,
   109  	})
   110  
   111  	logging := AddTestingService(c, s.State, "logging", AddTestingCharm(c, s.State, "logging"))
   112  	add(&params.ServiceInfo{
   113  		Name:     "logging",
   114  		CharmURL: serviceCharmURL(logging).String(),
   115  		OwnerTag: "user-admin",
   116  		Life:     params.Life(Alive.String()),
   117  		Config:   charm.Settings{},
   118  	})
   119  
   120  	eps, err := s.State.InferEndpoints([]string{"logging", "wordpress"})
   121  	c.Assert(err, gc.IsNil)
   122  	rel, err := s.State.AddRelation(eps...)
   123  	c.Assert(err, gc.IsNil)
   124  	add(&params.RelationInfo{
   125  		Key: "logging:logging-directory wordpress:logging-dir",
   126  		Id:  rel.Id(),
   127  		Endpoints: []params.Endpoint{
   128  			{ServiceName: "logging", Relation: charm.Relation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 1, Scope: "container"}},
   129  			{ServiceName: "wordpress", Relation: charm.Relation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}},
   130  	})
   131  
   132  	for i := 0; i < 2; i++ {
   133  		wu, err := wordpress.AddUnit()
   134  		c.Assert(err, gc.IsNil)
   135  		c.Assert(wu.Tag(), gc.Equals, fmt.Sprintf("unit-wordpress-%d", i))
   136  
   137  		m, err := s.State.AddMachine("quantal", JobHostUnits)
   138  		c.Assert(err, gc.IsNil)
   139  		c.Assert(m.Tag(), gc.Equals, fmt.Sprintf("machine-%d", i+1))
   140  
   141  		add(&params.UnitInfo{
   142  			Name:      fmt.Sprintf("wordpress/%d", i),
   143  			Service:   wordpress.Name(),
   144  			Series:    m.Series(),
   145  			MachineId: m.Id(),
   146  			Ports:     []instance.Port{},
   147  			Status:    params.StatusPending,
   148  		})
   149  		pairs := map[string]string{"name": fmt.Sprintf("bar %d", i)}
   150  		err = wu.SetAnnotations(pairs)
   151  		c.Assert(err, gc.IsNil)
   152  		add(&params.AnnotationInfo{
   153  			Tag:         fmt.Sprintf("unit-wordpress-%d", i),
   154  			Annotations: pairs,
   155  		})
   156  
   157  		err = m.SetProvisioned(instance.Id("i-"+m.Tag()), "fake_nonce", nil)
   158  		c.Assert(err, gc.IsNil)
   159  		err = m.SetStatus(params.StatusError, m.Tag(), nil)
   160  		c.Assert(err, gc.IsNil)
   161  		add(&params.MachineInfo{
   162  			Id:         fmt.Sprint(i + 1),
   163  			InstanceId: "i-" + m.Tag(),
   164  			Status:     params.StatusError,
   165  			StatusInfo: m.Tag(),
   166  		})
   167  		err = wu.AssignToMachine(m)
   168  		c.Assert(err, gc.IsNil)
   169  
   170  		deployer, ok := wu.DeployerTag()
   171  		c.Assert(ok, gc.Equals, true)
   172  		c.Assert(deployer, gc.Equals, fmt.Sprintf("machine-%d", i+1))
   173  
   174  		wru, err := rel.Unit(wu)
   175  		c.Assert(err, gc.IsNil)
   176  
   177  		// Create the subordinate unit as a side-effect of entering
   178  		// scope in the principal's relation-unit.
   179  		err = wru.EnterScope(nil)
   180  		c.Assert(err, gc.IsNil)
   181  
   182  		lu, err := s.State.Unit(fmt.Sprintf("logging/%d", i))
   183  		c.Assert(err, gc.IsNil)
   184  		c.Assert(lu.IsPrincipal(), gc.Equals, false)
   185  		deployer, ok = lu.DeployerTag()
   186  		c.Assert(ok, gc.Equals, true)
   187  		c.Assert(deployer, gc.Equals, fmt.Sprintf("unit-wordpress-%d", i))
   188  		add(&params.UnitInfo{
   189  			Name:    fmt.Sprintf("logging/%d", i),
   190  			Service: "logging",
   191  			Series:  "quantal",
   192  			Ports:   []instance.Port{},
   193  			Status:  params.StatusPending,
   194  		})
   195  	}
   196  	return
   197  }
   198  
   199  func serviceCharmURL(svc *Service) *charm.URL {
   200  	url, _ := svc.CharmURL()
   201  	return url
   202  }
   203  
   204  func assertEntitiesEqual(c *gc.C, got, want []params.EntityInfo) {
   205  	if len(got) == 0 {
   206  		got = nil
   207  	}
   208  	if len(want) == 0 {
   209  		want = nil
   210  	}
   211  	if reflect.DeepEqual(got, want) {
   212  		return
   213  	}
   214  	c.Errorf("entity mismatch; got len %d; want %d", len(got), len(want))
   215  	c.Logf("got:")
   216  	for _, e := range got {
   217  		c.Logf("\t%T %#v", e, e)
   218  	}
   219  	c.Logf("expected:")
   220  	for _, e := range want {
   221  		c.Logf("\t%T %#v", e, e)
   222  	}
   223  	c.FailNow()
   224  }
   225  
   226  func (s *storeManagerStateSuite) TestStateBackingGetAll(c *gc.C) {
   227  	expectEntities := s.setUpScenario(c)
   228  	b := newAllWatcherStateBacking(s.State)
   229  	all := multiwatcher.NewStore()
   230  	err := b.GetAll(all)
   231  	c.Assert(err, gc.IsNil)
   232  	var gotEntities entityInfoSlice = all.All()
   233  	sort.Sort(gotEntities)
   234  	sort.Sort(expectEntities)
   235  	assertEntitiesEqual(c, gotEntities, expectEntities)
   236  }
   237  
   238  var allWatcherChangedTests = []struct {
   239  	about          string
   240  	add            []params.EntityInfo
   241  	setUp          func(c *gc.C, st *State)
   242  	change         watcher.Change
   243  	expectContents []params.EntityInfo
   244  }{
   245  	// Machine changes
   246  	{
   247  		about: "no machine in state, no machine in store -> do nothing",
   248  		setUp: func(*gc.C, *State) {},
   249  		change: watcher.Change{
   250  			C:  "machines",
   251  			Id: "1",
   252  		},
   253  	}, {
   254  		about: "machine is removed if it's not in backing",
   255  		add:   []params.EntityInfo{&params.MachineInfo{Id: "1"}},
   256  		setUp: func(*gc.C, *State) {},
   257  		change: watcher.Change{
   258  			C:  "machines",
   259  			Id: "1",
   260  		},
   261  	}, {
   262  		about: "machine is added if it's in backing but not in Store",
   263  		setUp: func(c *gc.C, st *State) {
   264  			m, err := st.AddMachine("quantal", JobHostUnits)
   265  			c.Assert(err, gc.IsNil)
   266  			err = m.SetStatus(params.StatusError, "failure", nil)
   267  			c.Assert(err, gc.IsNil)
   268  		},
   269  		change: watcher.Change{
   270  			C:  "machines",
   271  			Id: "0",
   272  		},
   273  		expectContents: []params.EntityInfo{
   274  			&params.MachineInfo{
   275  				Id:         "0",
   276  				Status:     params.StatusError,
   277  				StatusInfo: "failure",
   278  			},
   279  		},
   280  	},
   281  	// Machine status changes
   282  	{
   283  		about: "machine is updated if it's in backing and in Store",
   284  		add: []params.EntityInfo{
   285  			&params.MachineInfo{
   286  				Id:         "0",
   287  				Status:     params.StatusError,
   288  				StatusInfo: "another failure",
   289  			},
   290  		},
   291  		setUp: func(c *gc.C, st *State) {
   292  			m, err := st.AddMachine("quantal", JobManageEnviron)
   293  			c.Assert(err, gc.IsNil)
   294  			err = m.SetProvisioned("i-0", "bootstrap_nonce", nil)
   295  			c.Assert(err, gc.IsNil)
   296  		},
   297  		change: watcher.Change{
   298  			C:  "machines",
   299  			Id: "0",
   300  		},
   301  		expectContents: []params.EntityInfo{
   302  			&params.MachineInfo{
   303  				Id:         "0",
   304  				InstanceId: "i-0",
   305  				Status:     params.StatusError,
   306  				StatusInfo: "another failure",
   307  			},
   308  		},
   309  	},
   310  	// Unit changes
   311  	{
   312  		about: "no unit in state, no unit in store -> do nothing",
   313  		setUp: func(c *gc.C, st *State) {},
   314  		change: watcher.Change{
   315  			C:  "units",
   316  			Id: "1",
   317  		},
   318  	}, {
   319  		about: "unit is removed if it's not in backing",
   320  		add:   []params.EntityInfo{&params.UnitInfo{Name: "wordpress/1"}},
   321  		setUp: func(*gc.C, *State) {},
   322  		change: watcher.Change{
   323  			C:  "units",
   324  			Id: "wordpress/1",
   325  		},
   326  	}, {
   327  		about: "unit is added if it's in backing but not in Store",
   328  		setUp: func(c *gc.C, st *State) {
   329  			wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   330  			u, err := wordpress.AddUnit()
   331  			c.Assert(err, gc.IsNil)
   332  			err = u.SetPublicAddress("public")
   333  			c.Assert(err, gc.IsNil)
   334  			err = u.SetPrivateAddress("private")
   335  			c.Assert(err, gc.IsNil)
   336  			err = u.OpenPort("tcp", 12345)
   337  			c.Assert(err, gc.IsNil)
   338  			m, err := st.AddMachine("quantal", JobHostUnits)
   339  			c.Assert(err, gc.IsNil)
   340  			err = u.AssignToMachine(m)
   341  			c.Assert(err, gc.IsNil)
   342  			err = u.SetStatus(params.StatusError, "failure", nil)
   343  			c.Assert(err, gc.IsNil)
   344  		},
   345  		change: watcher.Change{
   346  			C:  "units",
   347  			Id: "wordpress/0",
   348  		},
   349  		expectContents: []params.EntityInfo{
   350  			&params.UnitInfo{
   351  				Name:           "wordpress/0",
   352  				Service:        "wordpress",
   353  				Series:         "quantal",
   354  				PublicAddress:  "public",
   355  				PrivateAddress: "private",
   356  				MachineId:      "0",
   357  				Ports:          []instance.Port{{"tcp", 12345}},
   358  				Status:         params.StatusError,
   359  				StatusInfo:     "failure",
   360  			},
   361  		},
   362  	}, {
   363  		about: "unit is updated if it's in backing and in multiwatcher.Store",
   364  		add: []params.EntityInfo{&params.UnitInfo{
   365  			Name:       "wordpress/0",
   366  			Status:     params.StatusError,
   367  			StatusInfo: "another failure",
   368  		}},
   369  		setUp: func(c *gc.C, st *State) {
   370  			wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   371  			u, err := wordpress.AddUnit()
   372  			c.Assert(err, gc.IsNil)
   373  			err = u.SetPublicAddress("public")
   374  			c.Assert(err, gc.IsNil)
   375  			err = u.OpenPort("udp", 17070)
   376  			c.Assert(err, gc.IsNil)
   377  		},
   378  		change: watcher.Change{
   379  			C:  "units",
   380  			Id: "wordpress/0",
   381  		},
   382  		expectContents: []params.EntityInfo{
   383  			&params.UnitInfo{
   384  				Name:          "wordpress/0",
   385  				Service:       "wordpress",
   386  				Series:        "quantal",
   387  				PublicAddress: "public",
   388  				Ports:         []instance.Port{{"udp", 17070}},
   389  				Status:        params.StatusError,
   390  				StatusInfo:    "another failure",
   391  			},
   392  		},
   393  	},
   394  	// Service changes
   395  	{
   396  		about: "no service in state, no service in store -> do nothing",
   397  		setUp: func(c *gc.C, st *State) {},
   398  		change: watcher.Change{
   399  			C:  "services",
   400  			Id: "wordpress",
   401  		},
   402  	}, {
   403  		about: "service is removed if it's not in backing",
   404  		add:   []params.EntityInfo{&params.ServiceInfo{Name: "wordpress"}},
   405  		setUp: func(*gc.C, *State) {},
   406  		change: watcher.Change{
   407  			C:  "services",
   408  			Id: "wordpress",
   409  		},
   410  	}, {
   411  		about: "service is added if it's in backing but not in Store",
   412  		setUp: func(c *gc.C, st *State) {
   413  			wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   414  			err := wordpress.SetExposed()
   415  			c.Assert(err, gc.IsNil)
   416  			err = wordpress.SetMinUnits(42)
   417  			c.Assert(err, gc.IsNil)
   418  		},
   419  		change: watcher.Change{
   420  			C:  "services",
   421  			Id: "wordpress",
   422  		},
   423  		expectContents: []params.EntityInfo{
   424  			&params.ServiceInfo{
   425  				Name:     "wordpress",
   426  				Exposed:  true,
   427  				CharmURL: "local:quantal/quantal-wordpress-3",
   428  				OwnerTag: "user-admin",
   429  				Life:     params.Life(Alive.String()),
   430  				MinUnits: 42,
   431  				Config:   charm.Settings{},
   432  			},
   433  		},
   434  	}, {
   435  		about: "service is updated if it's in backing and in multiwatcher.Store",
   436  		add: []params.EntityInfo{&params.ServiceInfo{
   437  			Name:        "wordpress",
   438  			Exposed:     true,
   439  			CharmURL:    "local:quantal/quantal-wordpress-3",
   440  			MinUnits:    47,
   441  			Constraints: constraints.MustParse("mem=99M"),
   442  			Config:      charm.Settings{"blog-title": "boring"},
   443  		}},
   444  		setUp: func(c *gc.C, st *State) {
   445  			svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   446  			setServiceConfigAttr(c, svc, "blog-title", "boring")
   447  		},
   448  		change: watcher.Change{
   449  			C:  "services",
   450  			Id: "wordpress",
   451  		},
   452  		expectContents: []params.EntityInfo{
   453  			&params.ServiceInfo{
   454  				Name:        "wordpress",
   455  				CharmURL:    "local:quantal/quantal-wordpress-3",
   456  				OwnerTag:    "user-admin",
   457  				Life:        params.Life(Alive.String()),
   458  				Constraints: constraints.MustParse("mem=99M"),
   459  				Config:      charm.Settings{"blog-title": "boring"},
   460  			},
   461  		},
   462  	}, {
   463  		about: "service re-reads config when charm URL changes",
   464  		add: []params.EntityInfo{&params.ServiceInfo{
   465  			Name: "wordpress",
   466  			// Note: CharmURL has a different revision number from
   467  			// the wordpress revision in the testing repo.
   468  			CharmURL: "local:quantal/quantal-wordpress-2",
   469  			Config:   charm.Settings{"foo": "bar"},
   470  		}},
   471  		setUp: func(c *gc.C, st *State) {
   472  			svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   473  			setServiceConfigAttr(c, svc, "blog-title", "boring")
   474  		},
   475  		change: watcher.Change{
   476  			C:  "services",
   477  			Id: "wordpress",
   478  		},
   479  		expectContents: []params.EntityInfo{
   480  			&params.ServiceInfo{
   481  				Name:     "wordpress",
   482  				CharmURL: "local:quantal/quantal-wordpress-3",
   483  				OwnerTag: "user-admin",
   484  				Life:     params.Life(Alive.String()),
   485  				Config:   charm.Settings{"blog-title": "boring"},
   486  			},
   487  		},
   488  	},
   489  	// Relation changes
   490  	{
   491  		about: "no relation in state, no service in store -> do nothing",
   492  		setUp: func(c *gc.C, st *State) {},
   493  		change: watcher.Change{
   494  			C:  "relations",
   495  			Id: "logging:logging-directory wordpress:logging-dir",
   496  		},
   497  	}, {
   498  		about: "relation is removed if it's not in backing",
   499  		add:   []params.EntityInfo{&params.RelationInfo{Key: "logging:logging-directory wordpress:logging-dir"}},
   500  		setUp: func(*gc.C, *State) {},
   501  		change: watcher.Change{
   502  			C:  "relations",
   503  			Id: "logging:logging-directory wordpress:logging-dir",
   504  		},
   505  	}, {
   506  		about: "relation is added if it's in backing but not in Store",
   507  		setUp: func(c *gc.C, st *State) {
   508  			AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   509  
   510  			AddTestingService(c, st, "logging", AddTestingCharm(c, st, "logging"))
   511  			eps, err := st.InferEndpoints([]string{"logging", "wordpress"})
   512  			c.Assert(err, gc.IsNil)
   513  			_, err = st.AddRelation(eps...)
   514  			c.Assert(err, gc.IsNil)
   515  		},
   516  		change: watcher.Change{
   517  			C:  "relations",
   518  			Id: "logging:logging-directory wordpress:logging-dir",
   519  		},
   520  		expectContents: []params.EntityInfo{
   521  			&params.RelationInfo{
   522  				Key: "logging:logging-directory wordpress:logging-dir",
   523  				Endpoints: []params.Endpoint{
   524  					{ServiceName: "logging", Relation: charm.Relation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 1, Scope: "container"}},
   525  					{ServiceName: "wordpress", Relation: charm.Relation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}},
   526  			},
   527  		},
   528  	},
   529  	// Annotation changes
   530  	{
   531  		about: "no annotation in state, no annotation in store -> do nothing",
   532  		setUp: func(c *gc.C, st *State) {},
   533  		change: watcher.Change{
   534  			C:  "relations",
   535  			Id: "m#0",
   536  		},
   537  	}, {
   538  		about: "annotation is removed if it's not in backing",
   539  		add:   []params.EntityInfo{&params.AnnotationInfo{Tag: "machine-0"}},
   540  		setUp: func(*gc.C, *State) {},
   541  		change: watcher.Change{
   542  			C:  "annotations",
   543  			Id: "m#0",
   544  		},
   545  	}, {
   546  		about: "annotation is added if it's in backing but not in Store",
   547  		setUp: func(c *gc.C, st *State) {
   548  			m, err := st.AddMachine("quantal", JobHostUnits)
   549  			c.Assert(err, gc.IsNil)
   550  			err = m.SetAnnotations(map[string]string{"foo": "bar", "arble": "baz"})
   551  			c.Assert(err, gc.IsNil)
   552  		},
   553  		change: watcher.Change{
   554  			C:  "annotations",
   555  			Id: "m#0",
   556  		},
   557  		expectContents: []params.EntityInfo{
   558  			&params.AnnotationInfo{
   559  				Tag:         "machine-0",
   560  				Annotations: map[string]string{"foo": "bar", "arble": "baz"},
   561  			},
   562  		},
   563  	}, {
   564  		about: "annotation is updated if it's in backing and in multiwatcher.Store",
   565  		add: []params.EntityInfo{&params.AnnotationInfo{
   566  			Tag: "machine-0",
   567  			Annotations: map[string]string{
   568  				"arble":  "baz",
   569  				"foo":    "bar",
   570  				"pretty": "polly",
   571  			},
   572  		}},
   573  		setUp: func(c *gc.C, st *State) {
   574  			m, err := st.AddMachine("quantal", JobHostUnits)
   575  			c.Assert(err, gc.IsNil)
   576  			err = m.SetAnnotations(map[string]string{
   577  				"arble":  "khroomph",
   578  				"pretty": "",
   579  				"new":    "attr",
   580  			})
   581  			c.Assert(err, gc.IsNil)
   582  		},
   583  		change: watcher.Change{
   584  			C:  "annotations",
   585  			Id: "m#0",
   586  		},
   587  		expectContents: []params.EntityInfo{
   588  			&params.AnnotationInfo{
   589  				Tag: "machine-0",
   590  				Annotations: map[string]string{
   591  					"arble": "khroomph",
   592  					"new":   "attr",
   593  				},
   594  			},
   595  		},
   596  	},
   597  	// Unit status changes
   598  	{
   599  		about: "no unit in state -> do nothing",
   600  		setUp: func(c *gc.C, st *State) {},
   601  		change: watcher.Change{
   602  			C:  "statuses",
   603  			Id: "u#wordpress/0",
   604  		},
   605  	}, {
   606  		about: "no change if status is not in backing",
   607  		add: []params.EntityInfo{&params.UnitInfo{
   608  			Name:       "wordpress/0",
   609  			Status:     params.StatusError,
   610  			StatusInfo: "failure",
   611  		}},
   612  		setUp: func(*gc.C, *State) {},
   613  		change: watcher.Change{
   614  			C:  "statuses",
   615  			Id: "u#wordpress/0",
   616  		},
   617  		expectContents: []params.EntityInfo{
   618  			&params.UnitInfo{
   619  				Name:       "wordpress/0",
   620  				Status:     params.StatusError,
   621  				StatusInfo: "failure",
   622  			},
   623  		},
   624  	}, {
   625  		about: "status is changed if the unit exists in the store",
   626  		add: []params.EntityInfo{&params.UnitInfo{
   627  			Name:       "wordpress/0",
   628  			Status:     params.StatusError,
   629  			StatusInfo: "failure",
   630  		}},
   631  		setUp: func(c *gc.C, st *State) {
   632  			wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   633  			u, err := wordpress.AddUnit()
   634  			c.Assert(err, gc.IsNil)
   635  			err = u.SetStatus(params.StatusStarted, "", nil)
   636  			c.Assert(err, gc.IsNil)
   637  		},
   638  		change: watcher.Change{
   639  			C:  "statuses",
   640  			Id: "u#wordpress/0",
   641  		},
   642  		expectContents: []params.EntityInfo{
   643  			&params.UnitInfo{
   644  				Name:       "wordpress/0",
   645  				Status:     params.StatusStarted,
   646  				StatusData: params.StatusData{},
   647  			},
   648  		},
   649  	}, {
   650  		about: "status is changed with additional status data",
   651  		add: []params.EntityInfo{&params.UnitInfo{
   652  			Name:   "wordpress/0",
   653  			Status: params.StatusStarted,
   654  		}},
   655  		setUp: func(c *gc.C, st *State) {
   656  			wordpress := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   657  			u, err := wordpress.AddUnit()
   658  			c.Assert(err, gc.IsNil)
   659  			err = u.SetStatus(params.StatusError, "hook error", params.StatusData{
   660  				"1st-key": "one",
   661  				"2nd-key": 2,
   662  				"3rd-key": true,
   663  			})
   664  			c.Assert(err, gc.IsNil)
   665  		},
   666  		change: watcher.Change{
   667  			C:  "statuses",
   668  			Id: "u#wordpress/0",
   669  		},
   670  		expectContents: []params.EntityInfo{
   671  			&params.UnitInfo{
   672  				Name:       "wordpress/0",
   673  				Status:     params.StatusError,
   674  				StatusInfo: "hook error",
   675  				StatusData: params.StatusData{
   676  					"1st-key": "one",
   677  					"2nd-key": 2,
   678  					"3rd-key": true,
   679  				},
   680  			},
   681  		},
   682  	},
   683  	// Machine status changes
   684  	{
   685  		about: "no machine in state -> do nothing",
   686  		setUp: func(c *gc.C, st *State) {},
   687  		change: watcher.Change{
   688  			C:  "statuses",
   689  			Id: "m#0",
   690  		},
   691  	}, {
   692  		about: "no change if status is not in backing",
   693  		add: []params.EntityInfo{&params.MachineInfo{
   694  			Id:         "0",
   695  			Status:     params.StatusError,
   696  			StatusInfo: "failure",
   697  		}},
   698  		setUp: func(*gc.C, *State) {},
   699  		change: watcher.Change{
   700  			C:  "statuses",
   701  			Id: "m#0",
   702  		},
   703  		expectContents: []params.EntityInfo{&params.MachineInfo{
   704  			Id:         "0",
   705  			Status:     params.StatusError,
   706  			StatusInfo: "failure",
   707  		}},
   708  	}, {
   709  		about: "status is changed if the machine exists in the store",
   710  		add: []params.EntityInfo{&params.MachineInfo{
   711  			Id:         "0",
   712  			Status:     params.StatusError,
   713  			StatusInfo: "failure",
   714  		}},
   715  		setUp: func(c *gc.C, st *State) {
   716  			m, err := st.AddMachine("quantal", JobHostUnits)
   717  			c.Assert(err, gc.IsNil)
   718  			err = m.SetStatus(params.StatusStarted, "", nil)
   719  			c.Assert(err, gc.IsNil)
   720  		},
   721  		change: watcher.Change{
   722  			C:  "statuses",
   723  			Id: "m#0",
   724  		},
   725  		expectContents: []params.EntityInfo{
   726  			&params.MachineInfo{
   727  				Id:         "0",
   728  				Status:     params.StatusStarted,
   729  				StatusData: params.StatusData{},
   730  			},
   731  		},
   732  	},
   733  	// Service constraints changes
   734  	{
   735  		about: "no service in state -> do nothing",
   736  		setUp: func(c *gc.C, st *State) {},
   737  		change: watcher.Change{
   738  			C:  "constraints",
   739  			Id: "s#wordpress",
   740  		},
   741  	}, {
   742  		about: "no change if service is not in backing",
   743  		add: []params.EntityInfo{&params.ServiceInfo{
   744  			Name:        "wordpress",
   745  			Constraints: constraints.MustParse("mem=99M"),
   746  		}},
   747  		setUp: func(*gc.C, *State) {},
   748  		change: watcher.Change{
   749  			C:  "constraints",
   750  			Id: "s#wordpress",
   751  		},
   752  		expectContents: []params.EntityInfo{&params.ServiceInfo{
   753  			Name:        "wordpress",
   754  			Constraints: constraints.MustParse("mem=99M"),
   755  		}},
   756  	}, {
   757  		about: "status is changed if the service exists in the store",
   758  		add: []params.EntityInfo{&params.ServiceInfo{
   759  			Name:        "wordpress",
   760  			Constraints: constraints.MustParse("mem=99M cpu-cores=2 cpu-power=4"),
   761  		}},
   762  		setUp: func(c *gc.C, st *State) {
   763  			svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   764  			err := svc.SetConstraints(constraints.MustParse("mem=4G cpu-cores= arch=amd64"))
   765  			c.Assert(err, gc.IsNil)
   766  		},
   767  		change: watcher.Change{
   768  			C:  "constraints",
   769  			Id: "s#wordpress",
   770  		},
   771  		expectContents: []params.EntityInfo{
   772  			&params.ServiceInfo{
   773  				Name:        "wordpress",
   774  				Constraints: constraints.MustParse("mem=4G cpu-cores= arch=amd64"),
   775  			},
   776  		},
   777  	},
   778  	// Service config changes.
   779  	{
   780  		about: "no service in state -> do nothing",
   781  		setUp: func(c *gc.C, st *State) {},
   782  		change: watcher.Change{
   783  			C:  "settings",
   784  			Id: "s#wordpress#local:quantal/quantal-wordpress-3",
   785  		},
   786  	}, {
   787  		about: "no change if service is not in backing",
   788  		add: []params.EntityInfo{&params.ServiceInfo{
   789  			Name:     "wordpress",
   790  			CharmURL: "local:quantal/quantal-wordpress-3",
   791  		}},
   792  		setUp: func(*gc.C, *State) {},
   793  		change: watcher.Change{
   794  			C:  "settings",
   795  			Id: "s#wordpress#local:quantal/quantal-wordpress-3",
   796  		},
   797  		expectContents: []params.EntityInfo{&params.ServiceInfo{
   798  			Name:     "wordpress",
   799  			CharmURL: "local:quantal/quantal-wordpress-3",
   800  		}},
   801  	}, {
   802  		about: "service config is changed if service exists in the store with the same URL",
   803  		add: []params.EntityInfo{&params.ServiceInfo{
   804  			Name:     "wordpress",
   805  			CharmURL: "local:quantal/quantal-wordpress-3",
   806  			Config:   charm.Settings{"foo": "bar"},
   807  		}},
   808  		setUp: func(c *gc.C, st *State) {
   809  			svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   810  			setServiceConfigAttr(c, svc, "blog-title", "foo")
   811  		},
   812  		change: watcher.Change{
   813  			C:  "settings",
   814  			Id: "s#wordpress#local:quantal/quantal-wordpress-3",
   815  		},
   816  		expectContents: []params.EntityInfo{
   817  			&params.ServiceInfo{
   818  				Name:     "wordpress",
   819  				CharmURL: "local:quantal/quantal-wordpress-3",
   820  				Config:   charm.Settings{"blog-title": "foo"},
   821  			},
   822  		},
   823  	}, {
   824  		about: "service config is unescaped when reading from the backing store",
   825  		add: []params.EntityInfo{&params.ServiceInfo{
   826  			Name:     "wordpress",
   827  			CharmURL: "local:quantal/quantal-wordpress-3",
   828  			Config:   charm.Settings{"key.dotted": "bar"},
   829  		}},
   830  		setUp: func(c *gc.C, st *State) {
   831  			testCharm := AddCustomCharm(
   832  				c, st, "wordpress",
   833  				"config.yaml", dottedConfig,
   834  				"quantal", 3)
   835  			svc := AddTestingService(c, st, "wordpress", testCharm)
   836  			setServiceConfigAttr(c, svc, "key.dotted", "foo")
   837  		},
   838  		change: watcher.Change{
   839  			C:  "settings",
   840  			Id: "s#wordpress#local:quantal/quantal-wordpress-3",
   841  		},
   842  		expectContents: []params.EntityInfo{
   843  			&params.ServiceInfo{
   844  				Name:     "wordpress",
   845  				CharmURL: "local:quantal/quantal-wordpress-3",
   846  				Config:   charm.Settings{"key.dotted": "foo"},
   847  			},
   848  		},
   849  	}, {
   850  		about: "service config is unchanged if service exists in the store with a different URL",
   851  		add: []params.EntityInfo{&params.ServiceInfo{
   852  			Name:     "wordpress",
   853  			CharmURL: "local:quantal/quantal-wordpress-2", // Note different revno.
   854  			Config:   charm.Settings{"foo": "bar"},
   855  		}},
   856  		setUp: func(c *gc.C, st *State) {
   857  			svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   858  			setServiceConfigAttr(c, svc, "blog-title", "foo")
   859  		},
   860  		change: watcher.Change{
   861  			C:  "settings",
   862  			Id: "s#wordpress#local:quantal/quantal-wordpress-3",
   863  		},
   864  		expectContents: []params.EntityInfo{
   865  			&params.ServiceInfo{
   866  				Name:     "wordpress",
   867  				CharmURL: "local:quantal/quantal-wordpress-2",
   868  				Config:   charm.Settings{"foo": "bar"},
   869  			},
   870  		},
   871  	}, {
   872  		about: "non-service config change is ignored",
   873  		setUp: func(*gc.C, *State) {},
   874  		change: watcher.Change{
   875  			C:  "settings",
   876  			Id: "m#0",
   877  		},
   878  	}, {
   879  		about: "service config change with no charm url is ignored",
   880  		setUp: func(*gc.C, *State) {},
   881  		change: watcher.Change{
   882  			C:  "settings",
   883  			Id: "s#foo",
   884  		},
   885  	},
   886  }
   887  
   888  func setServiceConfigAttr(c *gc.C, svc *Service, attr string, val interface{}) {
   889  	err := svc.UpdateConfigSettings(charm.Settings{attr: val})
   890  	c.Assert(err, gc.IsNil)
   891  }
   892  
   893  func (s *storeManagerStateSuite) TestChanged(c *gc.C) {
   894  	collections := map[string]*mgo.Collection{
   895  		"machines":    s.State.machines,
   896  		"units":       s.State.units,
   897  		"services":    s.State.services,
   898  		"relations":   s.State.relations,
   899  		"annotations": s.State.annotations,
   900  		"statuses":    s.State.statuses,
   901  		"constraints": s.State.constraints,
   902  		"settings":    s.State.settings,
   903  	}
   904  	for i, test := range allWatcherChangedTests {
   905  		c.Logf("test %d. %s", i, test.about)
   906  		b := newAllWatcherStateBacking(s.State)
   907  		all := multiwatcher.NewStore()
   908  		for _, info := range test.add {
   909  			all.Update(info)
   910  		}
   911  		test.setUp(c, s.State)
   912  		c.Logf("done set up")
   913  		ch := test.change
   914  		ch.C = collections[ch.C].Name
   915  		err := b.Changed(all, test.change)
   916  		c.Assert(err, gc.IsNil)
   917  		assertEntitiesEqual(c, all.All(), test.expectContents)
   918  		s.Reset(c)
   919  	}
   920  }
   921  
   922  // StateWatcher tests the integration of the state watcher
   923  // with the state-based backing. Most of the logic is tested elsewhere -
   924  // this just tests end-to-end.
   925  func (s *storeManagerStateSuite) TestStateWatcher(c *gc.C) {
   926  	m0, err := s.State.AddMachine("quantal", JobManageEnviron)
   927  	c.Assert(err, gc.IsNil)
   928  	c.Assert(m0.Id(), gc.Equals, "0")
   929  
   930  	m1, err := s.State.AddMachine("quantal", JobHostUnits)
   931  	c.Assert(err, gc.IsNil)
   932  	c.Assert(m1.Id(), gc.Equals, "1")
   933  
   934  	b := newAllWatcherStateBacking(s.State)
   935  	aw := multiwatcher.NewStoreManager(b)
   936  	defer aw.Stop()
   937  	w := multiwatcher.NewWatcher(aw)
   938  	s.State.StartSync()
   939  	checkNext(c, w, b, []params.Delta{{
   940  		Entity: &params.MachineInfo{
   941  			Id:     "0",
   942  			Status: params.StatusPending,
   943  		},
   944  	}, {
   945  		Entity: &params.MachineInfo{
   946  			Id:     "1",
   947  			Status: params.StatusPending,
   948  		},
   949  	}}, "")
   950  
   951  	// Make some changes to the state.
   952  	err = m0.SetProvisioned("i-0", "bootstrap_nonce", nil)
   953  	c.Assert(err, gc.IsNil)
   954  	err = m1.Destroy()
   955  	c.Assert(err, gc.IsNil)
   956  	err = m1.EnsureDead()
   957  	c.Assert(err, gc.IsNil)
   958  	err = m1.Remove()
   959  	c.Assert(err, gc.IsNil)
   960  	m2, err := s.State.AddMachine("quantal", JobHostUnits)
   961  	c.Assert(err, gc.IsNil)
   962  	c.Assert(m2.Id(), gc.Equals, "2")
   963  	s.State.StartSync()
   964  
   965  	// Check that we see the changes happen within a
   966  	// reasonable time.
   967  	var deltas []params.Delta
   968  	for {
   969  		d, err := getNext(c, w, 100*time.Millisecond)
   970  		if errors.Cause(err) == errTimeout {
   971  			break
   972  		}
   973  		c.Assert(err, gc.IsNil)
   974  		deltas = append(deltas, d...)
   975  	}
   976  	checkDeltasEqual(c, b, deltas, []params.Delta{{
   977  		Removed: true,
   978  		Entity: &params.MachineInfo{
   979  			Id:     "1",
   980  			Status: params.StatusPending,
   981  		},
   982  	}, {
   983  		Entity: &params.MachineInfo{
   984  			Id:     "2",
   985  			Status: params.StatusPending,
   986  		},
   987  	}, {
   988  		Entity: &params.MachineInfo{
   989  			Id:         "0",
   990  			InstanceId: "i-0",
   991  			Status:     params.StatusPending,
   992  		},
   993  	}})
   994  
   995  	err = w.Stop()
   996  	c.Assert(err, gc.IsNil)
   997  
   998  	_, err = w.Next()
   999  	c.Assert(errors.Cause(err), gc.Equals, multiwatcher.ErrWatcherStopped)
  1000  }
  1001  
  1002  type entityInfoSlice []params.EntityInfo
  1003  
  1004  func (s entityInfoSlice) Len() int      { return len(s) }
  1005  func (s entityInfoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
  1006  func (s entityInfoSlice) Less(i, j int) bool {
  1007  	id0, id1 := s[i].EntityId(), s[j].EntityId()
  1008  	if id0.Kind != id1.Kind {
  1009  		return id0.Kind < id1.Kind
  1010  	}
  1011  	switch id := id0.Id.(type) {
  1012  	case string:
  1013  		return id < id1.Id.(string)
  1014  	default:
  1015  	}
  1016  	panic("unexpected entity id type")
  1017  }
  1018  
  1019  var errTimeout = errors.New("no change received in sufficient time")
  1020  
  1021  func getNext(c *gc.C, w *multiwatcher.Watcher, timeout time.Duration) ([]params.Delta, error) {
  1022  	var deltas []params.Delta
  1023  	var err error
  1024  	ch := make(chan struct{}, 1)
  1025  	go func() {
  1026  		deltas, err = w.Next()
  1027  		ch <- struct{}{}
  1028  	}()
  1029  	select {
  1030  	case <-ch:
  1031  		return deltas, err
  1032  	case <-time.After(1 * time.Second):
  1033  	}
  1034  	return nil, errTimeout
  1035  }
  1036  
  1037  func checkNext(c *gc.C, w *multiwatcher.Watcher, b multiwatcher.Backing, deltas []params.Delta, expectErr string) {
  1038  	d, err := getNext(c, w, 1*time.Second)
  1039  	if expectErr != "" {
  1040  		c.Check(err, gc.ErrorMatches, expectErr)
  1041  		return
  1042  	}
  1043  	checkDeltasEqual(c, b, d, deltas)
  1044  }
  1045  
  1046  // deltas are returns in arbitrary order, so we compare
  1047  // them as sets.
  1048  func checkDeltasEqual(c *gc.C, b multiwatcher.Backing, d0, d1 []params.Delta) {
  1049  	c.Check(deltaMap(d0, b), gc.DeepEquals, deltaMap(d1, b))
  1050  }
  1051  
  1052  func deltaMap(deltas []params.Delta, b multiwatcher.Backing) map[multiwatcher.InfoId]params.EntityInfo {
  1053  	m := make(map[multiwatcher.InfoId]params.EntityInfo)
  1054  	for _, d := range deltas {
  1055  		id := d.Entity.EntityId()
  1056  		if _, ok := m[id]; ok {
  1057  			panic(errors.Newf("%v mentioned twice in delta set", id))
  1058  		}
  1059  		if d.Removed {
  1060  			m[id] = nil
  1061  		} else {
  1062  			m[id] = d.Entity
  1063  		}
  1064  	}
  1065  	return m
  1066  }