7
"labix.org/v2/mgo/bson"
8
. "launchpad.net/gocheck"
9
"launchpad.net/juju-core/charm"
10
"launchpad.net/juju-core/log"
11
"launchpad.net/juju-core/store"
12
"launchpad.net/juju-core/testing"
19
func Test(t *stdtesting.T) {
23
var _ = Suite(&StoreSuite{})
24
var _ = Suite(&TrivialSuite{})
26
type StoreSuite struct {
32
type TrivialSuite struct{}
34
func (s *StoreSuite) SetUpSuite(c *C) {
35
s.MgoSuite.SetUpSuite(c)
36
s.HTTPSuite.SetUpSuite(c)
39
func (s *StoreSuite) TearDownSuite(c *C) {
40
s.HTTPSuite.TearDownSuite(c)
41
s.MgoSuite.TearDownSuite(c)
44
func (s *StoreSuite) SetUpTest(c *C) {
45
s.MgoSuite.SetUpTest(c)
47
s.store, err = store.Open(s.Addr)
53
func (s *StoreSuite) TearDownTest(c *C) {
54
s.HTTPSuite.TearDownTest(c)
58
s.MgoSuite.TearDownTest(c)
61
// FakeCharmDir is a charm that implements the interface that the
62
// store publisher cares about.
63
type FakeCharmDir struct {
64
revision interface{} // so we can tell if it's not set.
68
func (d *FakeCharmDir) Meta() *charm.Meta {
71
Summary: "Fake charm for testing purposes.",
72
Description: "This is a fake charm for testing purposes.\n",
73
Provides: make(map[string]charm.Relation),
74
Requires: make(map[string]charm.Relation),
75
Peers: make(map[string]charm.Relation),
79
func (d *FakeCharmDir) Config() *charm.Config {
80
return &charm.Config{make(map[string]charm.Option)}
83
func (d *FakeCharmDir) SetRevision(revision int) {
87
func (d *FakeCharmDir) BundleTo(w io.Writer) error {
88
if d.error == "beforeWrite" {
89
return fmt.Errorf(d.error)
91
_, err := w.Write([]byte(fmt.Sprintf("charm-revision-%v", d.revision)))
92
if d.error == "afterWrite" {
93
return fmt.Errorf(d.error)
98
func (s *StoreSuite) TestCharmPublisherWithRevisionedURL(c *C) {
99
urls := []*charm.URL{charm.MustParseURL("cs:oneiric/wordpress-0")}
100
pub, err := s.store.CharmPublisher(urls, "some-digest")
101
c.Assert(err, ErrorMatches, "CharmPublisher: got charm URL with revision: cs:oneiric/wordpress-0")
105
func (s *StoreSuite) TestCharmPublisher(c *C) {
106
urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
107
urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
108
urls := []*charm.URL{urlA, urlB}
110
pub, err := s.store.CharmPublisher(urls, "some-digest")
112
c.Assert(pub.Revision(), Equals, 0)
114
err = pub.Publish(testing.Charms.ClonedDir(c.MkDir(), "dummy"))
117
for _, url := range urls {
118
info, rc, err := s.store.OpenCharm(url)
120
c.Assert(info.Revision(), Equals, 0)
121
c.Assert(info.Digest(), Equals, "some-digest")
122
data, err := ioutil.ReadAll(rc)
126
bundle, err := charm.ReadBundleBytes(data)
129
// The same information must be available by reading the
130
// full charm data...
131
c.Assert(bundle.Meta().Name, Equals, "dummy")
132
c.Assert(bundle.Config().Options["title"].Default, Equals, "My Title")
134
// ... and the queriable details.
135
c.Assert(info.Meta().Name, Equals, "dummy")
136
c.Assert(info.Config().Options["title"].Default, Equals, "My Title")
138
info2, err := s.store.CharmInfo(url)
140
c.Assert(info2, DeepEquals, info)
144
func (s *StoreSuite) TestCharmPublishError(c *C) {
145
url := charm.MustParseURL("cs:oneiric/wordpress")
146
urls := []*charm.URL{url}
148
// Publish one successfully to bump the revision so we can
149
// make sure it is being correctly set below.
150
pub, err := s.store.CharmPublisher(urls, "one-digest")
152
c.Assert(pub.Revision(), Equals, 0)
153
err = pub.Publish(&FakeCharmDir{})
156
pub, err = s.store.CharmPublisher(urls, "another-digest")
158
c.Assert(pub.Revision(), Equals, 1)
159
err = pub.Publish(&FakeCharmDir{error: "beforeWrite"})
160
c.Assert(err, ErrorMatches, "beforeWrite")
162
pub, err = s.store.CharmPublisher(urls, "another-digest")
164
c.Assert(pub.Revision(), Equals, 1)
165
err = pub.Publish(&FakeCharmDir{error: "afterWrite"})
166
c.Assert(err, ErrorMatches, "afterWrite")
168
// Still at the original charm revision that succeeded first.
169
info, err := s.store.CharmInfo(url)
171
c.Assert(info.Revision(), Equals, 0)
172
c.Assert(info.Digest(), Equals, "one-digest")
175
func (s *StoreSuite) TestCharmInfoNotFound(c *C) {
176
info, err := s.store.CharmInfo(charm.MustParseURL("cs:oneiric/wordpress"))
177
c.Assert(err, Equals, store.ErrNotFound)
178
c.Assert(info, IsNil)
181
func (s *StoreSuite) TestRevisioning(c *C) {
182
urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
183
urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
184
urls := []*charm.URL{urlA, urlB}
190
{urls[0:], "charm-revision-0"},
191
{urls[1:], "charm-revision-1"},
192
{urls[0:], "charm-revision-2"},
195
for i, t := range tests {
196
pub, err := s.store.CharmPublisher(t.urls, fmt.Sprintf("digest-%d", i))
198
c.Assert(pub.Revision(), Equals, i)
200
err = pub.Publish(&FakeCharmDir{})
204
for i, t := range tests {
205
for _, url := range t.urls {
206
url = url.WithRevision(i)
207
info, rc, err := s.store.OpenCharm(url)
209
data, err := ioutil.ReadAll(rc)
211
c.Assert(info.Revision(), Equals, i)
212
c.Assert(url.Revision, Equals, i) // Untouched.
213
c.Assert(cerr, IsNil)
214
c.Assert(string(data), Equals, string(t.data))
219
info, rc, err := s.store.OpenCharm(urlA.WithRevision(1))
220
c.Assert(err, Equals, store.ErrNotFound)
221
c.Assert(info, IsNil)
225
func (s *StoreSuite) TestLockUpdates(c *C) {
226
urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
227
urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
228
urls := []*charm.URL{urlA, urlB}
230
// Lock update of just B to force a partial conflict.
231
lock1, err := s.store.LockUpdates(urls[1:])
234
// Partially conflicts with locked update above.
235
lock2, err := s.store.LockUpdates(urls)
236
c.Check(err, Equals, store.ErrUpdateConflict)
237
c.Check(lock2, IsNil)
241
// Trying again should work since lock1 was released.
242
lock3, err := s.store.LockUpdates(urls)
247
func (s *StoreSuite) TestLockUpdatesExpires(c *C) {
248
urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
249
urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
250
urls := []*charm.URL{urlA, urlB}
252
// Initiate an update of B only to force a partial conflict.
253
lock1, err := s.store.LockUpdates(urls[1:])
256
// Hack time to force an expiration.
257
locks := s.Session.DB("juju").C("locks")
258
selector := bson.M{"_id": urlB.String()}
259
update := bson.M{"time": bson.Now().Add(-store.UpdateTimeout - 10e9)}
260
err = locks.Update(selector, update)
263
// Works due to expiration of previous lock.
264
lock2, err := s.store.LockUpdates(urls)
268
// The expired lock was forcefully killed. Unlocking it must
269
// not interfere with lock2 which is still alive.
272
// The above statement was a NOOP and lock2 is still in effect,
273
// so attempting another lock must necessarily fail.
274
lock3, err := s.store.LockUpdates(urls)
275
c.Check(err == store.ErrUpdateConflict, Equals, true)
276
c.Check(lock3, IsNil)
279
func (s *StoreSuite) TestConflictingUpdate(c *C) {
280
// This test checks that if for whatever reason the locking
281
// safety-net fails, adding two charms in parallel still
282
// results in a sane outcome.
283
url := charm.MustParseURL("cs:oneiric/wordpress")
284
urls := []*charm.URL{url}
286
pub1, err := s.store.CharmPublisher(urls, "some-digest")
288
c.Assert(pub1.Revision(), Equals, 0)
290
pub2, err := s.store.CharmPublisher(urls, "some-digest")
292
c.Assert(pub2.Revision(), Equals, 0)
294
// The first publishing attempt should work.
295
err = pub2.Publish(&FakeCharmDir{})
298
// Attempting to finish the second attempt should break,
299
// since it lost the race and the given revision is already
301
err = pub1.Publish(&FakeCharmDir{})
302
c.Assert(err, Equals, store.ErrUpdateConflict)
305
func (s *StoreSuite) TestRedundantUpdate(c *C) {
306
urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
307
urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
308
urls := []*charm.URL{urlA, urlB}
310
pub, err := s.store.CharmPublisher(urls, "digest-0")
312
c.Assert(pub.Revision(), Equals, 0)
313
err = pub.Publish(&FakeCharmDir{})
316
// All charms are already on digest-0.
317
pub, err = s.store.CharmPublisher(urls, "digest-0")
318
c.Assert(err, ErrorMatches, "charm is up-to-date")
319
c.Assert(err, Equals, store.ErrRedundantUpdate)
322
// Now add a second revision just for wordpress-b.
323
pub, err = s.store.CharmPublisher(urls[1:], "digest-1")
325
c.Assert(pub.Revision(), Equals, 1)
326
err = pub.Publish(&FakeCharmDir{})
329
// Same digest bumps revision because one of them was old.
330
pub, err = s.store.CharmPublisher(urls, "digest-1")
332
c.Assert(pub.Revision(), Equals, 2)
333
err = pub.Publish(&FakeCharmDir{})
337
const fakeRevZeroSha = "319095521ac8a62fa1e8423351973512ecca8928c9f62025e37de57c9ef07a53"
339
func (s *StoreSuite) TestCharmBundleData(c *C) {
340
url := charm.MustParseURL("cs:oneiric/wordpress")
341
urls := []*charm.URL{url}
343
pub, err := s.store.CharmPublisher(urls, "key")
345
c.Assert(pub.Revision(), Equals, 0)
347
err = pub.Publish(&FakeCharmDir{})
350
info, rc, err := s.store.OpenCharm(url)
352
c.Check(info.BundleSha256(), Equals, fakeRevZeroSha)
353
c.Check(info.BundleSize(), Equals, int64(len("charm-revision-0")))
358
func (s *StoreSuite) TestLogCharmEventWithRevisionedURL(c *C) {
359
url := charm.MustParseURL("cs:oneiric/wordpress-0")
360
event := &store.CharmEvent{
361
Kind: store.EventPublishError,
362
Digest: "some-digest",
363
URLs: []*charm.URL{url},
365
err := s.store.LogCharmEvent(event)
366
c.Assert(err, ErrorMatches, "LogCharmEvent: got charm URL with revision: cs:oneiric/wordpress-0")
368
// This may work in the future, but not now.
369
event, err = s.store.CharmEvent(url, "some-digest")
370
c.Assert(err, ErrorMatches, "CharmEvent: got charm URL with revision: cs:oneiric/wordpress-0")
371
c.Assert(event, IsNil)
374
func (s *StoreSuite) TestLogCharmEvent(c *C) {
375
url1 := charm.MustParseURL("cs:oneiric/wordpress")
376
url2 := charm.MustParseURL("cs:oneiric/mysql")
377
urls := []*charm.URL{url1, url2}
379
event1 := &store.CharmEvent{
380
Kind: store.EventPublished,
384
Warnings: []string{"A warning."},
385
Time: time.Unix(1, 0),
387
event2 := &store.CharmEvent{
388
Kind: store.EventPublished,
392
Time: time.Unix(1, 0),
394
event3 := &store.CharmEvent{
395
Kind: store.EventPublishError,
397
Errors: []string{"An error."},
401
for _, event := range []*store.CharmEvent{event1, event2, event3} {
402
err := s.store.LogCharmEvent(event)
406
events := s.Session.DB("juju").C("events")
407
var s1, s2 map[string]interface{}
409
err := events.Find(bson.M{"digest": "revKey1"}).One(&s1)
411
c.Assert(s1["kind"], Equals, int(store.EventPublished))
412
c.Assert(s1["urls"], DeepEquals, []interface{}{"cs:oneiric/wordpress", "cs:oneiric/mysql"})
413
c.Assert(s1["warnings"], DeepEquals, []interface{}{"A warning."})
414
c.Assert(s1["errors"], IsNil)
415
c.Assert(s1["time"], DeepEquals, time.Unix(1, 0))
417
err = events.Find(bson.M{"digest": "revKey2", "kind": store.EventPublishError}).One(&s2)
419
c.Assert(s2["urls"], DeepEquals, []interface{}{"cs:oneiric/wordpress"})
420
c.Assert(s2["warnings"], IsNil)
421
c.Assert(s2["errors"], DeepEquals, []interface{}{"An error."})
422
c.Assert(s2["time"].(time.Time).After(bson.Now().Add(-10e9)), Equals, true)
424
// Mongo stores timestamps in milliseconds, so chop
425
// off the extra bits for comparison.
426
event3.Time = time.Unix(0, event3.Time.UnixNano()/1e6*1e6)
428
event, err := s.store.CharmEvent(urls[0], "revKey2")
430
c.Assert(event, DeepEquals, event3)
432
event, err = s.store.CharmEvent(urls[1], "revKey1")
434
c.Assert(event, DeepEquals, event1)
436
event, err = s.store.CharmEvent(urls[1], "revKeyX")
437
c.Assert(err, Equals, store.ErrNotFound)
438
c.Assert(event, IsNil)
441
func (s *StoreSuite) TestSumCounters(c *C) {
442
req := store.CounterRequest{Key: []string{"a"}}
443
cs, err := s.store.Counters(&req)
445
c.Assert(cs, DeepEquals, []store.Counter{{Key: req.Key, Count: 0}})
447
for i := 0; i < 10; i++ {
448
err := s.store.IncCounter([]string{"a", "b", "c"})
451
for i := 0; i < 7; i++ {
452
s.store.IncCounter([]string{"a", "b"})
455
for i := 0; i < 3; i++ {
456
s.store.IncCounter([]string{"a", "z", "b"})
465
{[]string{"a", "b", "c"}, false, 10},
466
{[]string{"a", "b"}, false, 7},
467
{[]string{"a", "z", "b"}, false, 3},
468
{[]string{"a", "b", "c"}, true, 0},
469
{[]string{"a", "b", "c", "d"}, false, 0},
470
{[]string{"a", "b"}, true, 10},
471
{[]string{"a"}, true, 20},
472
{[]string{"b"}, true, 0},
475
for _, t := range tests {
476
c.Logf("Test: %#v\n", t)
477
req = store.CounterRequest{Key: t.key, Prefix: t.prefix}
478
cs, err := s.store.Counters(&req)
480
c.Assert(cs, DeepEquals, []store.Counter{{Key: t.key, Prefix: t.prefix, Count: t.result}})
483
// High-level interface works. Now check that the data is
485
counters := s.Session.DB("juju").C("stat.counters")
486
docs1, err := counters.Count()
488
if docs1 != 3 && docs1 != 4 {
489
fmt.Errorf("Expected 3 or 4 docs in counters collection, got %d", docs1)
492
// Hack times so that the next operation adds another document.
493
err = counters.Update(nil, bson.D{{"$set", bson.D{{"t", 1}}}})
496
err = s.store.IncCounter([]string{"a", "b", "c"})
499
docs2, err := counters.Count()
501
c.Assert(docs2, Equals, docs1+1)
503
req = store.CounterRequest{Key: []string{"a", "b", "c"}}
504
cs, err = s.store.Counters(&req)
506
c.Assert(cs, DeepEquals, []store.Counter{{Key: req.Key, Count: 11}})
508
req = store.CounterRequest{Key: []string{"a"}, Prefix: true}
509
cs, err = s.store.Counters(&req)
511
c.Assert(cs, DeepEquals, []store.Counter{{Key: req.Key, Prefix: true, Count: 21}})
514
func (s *StoreSuite) TestCountersReadOnlySum(c *C) {
515
// Summing up an unknown key shouldn't add the key to the database.
516
req := store.CounterRequest{Key: []string{"a", "b", "c"}}
517
_, err := s.store.Counters(&req)
520
tokens := s.Session.DB("juju").C("stat.tokens")
521
n, err := tokens.Count()
523
c.Assert(n, Equals, 0)
526
func (s *StoreSuite) TestCountersTokenCaching(c *C) {
527
assertSum := func(i int, want int64) {
528
req := store.CounterRequest{Key: []string{strconv.Itoa(i)}}
529
cs, err := s.store.Counters(&req)
531
c.Assert(cs[0].Count, Equals, want)
537
// All of these will be cached, as we have two generations
538
// of genSize entries each.
539
for i := 0; i < genSize*2; i++ {
540
err := s.store.IncCounter([]string{strconv.Itoa(i)})
544
// Now go behind the scenes and corrupt all the tokens.
545
tokens := s.Session.DB("juju").C("stat.tokens")
546
iter := tokens.Find(nil).Iter()
552
err := tokens.UpdateId(t.Id, bson.M{"$set": bson.M{"t": "corrupted" + t.Token}})
555
c.Assert(iter.Err(), IsNil)
557
// We can consult the counters for the cached entries still.
558
// First, check that the newest generation is good.
559
for i := genSize; i < genSize*2; i++ {
563
// Now, we can still access a single entry of the older generation,
564
// but this will cause the generations to flip and thus the rest
565
// of the old generation will go away as the top half of the
566
// entries is turned into the old generation.
569
// Now we've lost access to the rest of the old generation.
570
for i := 1; i < genSize; i++ {
574
// But we still have all of the top half available since it was
575
// moved into the old generation.
576
for i := genSize; i < genSize*2; i++ {
581
func (s *StoreSuite) TestCounterTokenUniqueness(c *C) {
582
var wg0, wg1 sync.WaitGroup
585
for i := 0; i < 10; i++ {
590
err := s.store.IncCounter([]string{"a"})
596
req := store.CounterRequest{Key: []string{"a"}}
597
cs, err := s.store.Counters(&req)
599
c.Assert(cs[0].Count, Equals, int64(10))
602
func (s *StoreSuite) TestListCounters(c *C) {
604
{"c", "b", "a"}, // Assign internal id c < id b < id a, to make sorting slightly trickier.
618
for _, key := range incs {
619
err := s.store.IncCounter(key)
625
result []store.Counter
630
{Key: []string{"a", "b"}, Prefix: true, Count: 4},
631
{Key: []string{"a", "f"}, Prefix: true, Count: 2},
632
{Key: []string{"a", "b"}, Prefix: false, Count: 1},
633
{Key: []string{"a", "c"}, Prefix: false, Count: 1},
634
{Key: []string{"a", "i"}, Prefix: false, Count: 1},
635
{Key: []string{"a", "i"}, Prefix: true, Count: 1},
640
{Key: []string{"a", "b", "c"}, Prefix: false, Count: 2},
641
{Key: []string{"a", "b", "d"}, Prefix: false, Count: 1},
642
{Key: []string{"a", "b", "e"}, Prefix: false, Count: 1},
646
[]store.Counter(nil),
650
// Use a different store to exercise cache filling.
651
st, err := store.Open(s.Addr)
655
for i := range tests {
656
req := &store.CounterRequest{Key: tests[i].prefix, Prefix: true, List: true}
657
result, err := st.Counters(req)
659
c.Assert(result, DeepEquals, tests[i].result)
663
func (s *StoreSuite) TestListCountersBy(c *C) {
671
{[]string{"a", "b"}, 1},
672
{[]string{"a", "c"}, 1},
674
{[]string{"a", "b"}, 3},
677
{[]string{"a", "c", "d"}, 9},
678
{[]string{"a", "c", "e"}, 9},
679
{[]string{"a", "c", "f"}, 9},
682
day := func(i int) time.Time {
683
return time.Date(2012, time.May, i, 0, 0, 0, 0, time.UTC)
686
counters := s.Session.DB("juju").C("stat.counters")
687
for i, inc := range incs {
688
err := s.store.IncCounter(inc.key)
691
// Hack time so counters are assigned to 2012-05-<day>
692
filter := bson.M{"t": bson.M{"$gt": store.TimeToStamp(time.Date(2013, time.January, 1, 0, 0, 0, 0, time.UTC))}}
693
stamp := store.TimeToStamp(day(inc.day))
694
stamp += int32(i) * 60 // Make every entry unique.
695
err = counters.Update(filter, bson.D{{"$set", bson.D{{"t", stamp}}}})
700
request store.CounterRequest
701
result []store.Counter
704
store.CounterRequest{
711
{Key: []string{"a"}, Prefix: false, Count: 2, Time: day(1)},
712
{Key: []string{"a"}, Prefix: false, Count: 1, Time: day(3)},
715
store.CounterRequest{
722
{Key: []string{"a"}, Prefix: true, Count: 2, Time: day(1)},
723
{Key: []string{"a"}, Prefix: true, Count: 1, Time: day(3)},
724
{Key: []string{"a"}, Prefix: true, Count: 3, Time: day(9)},
727
store.CounterRequest{
735
{Key: []string{"a"}, Prefix: true, Count: 1, Time: day(3)},
736
{Key: []string{"a"}, Prefix: true, Count: 3, Time: day(9)},
739
store.CounterRequest{
747
{Key: []string{"a"}, Prefix: true, Count: 2, Time: day(1)},
748
{Key: []string{"a"}, Prefix: true, Count: 1, Time: day(3)},
751
store.CounterRequest{
760
{Key: []string{"a"}, Prefix: true, Count: 1, Time: day(3)},
763
store.CounterRequest{
770
{Key: []string{"a", "b"}, Prefix: false, Count: 1, Time: day(1)},
771
{Key: []string{"a", "c"}, Prefix: false, Count: 1, Time: day(1)},
772
{Key: []string{"a", "b"}, Prefix: false, Count: 1, Time: day(3)},
773
{Key: []string{"a", "c"}, Prefix: true, Count: 3, Time: day(9)},
776
store.CounterRequest{
783
{Key: []string{"a"}, Prefix: true, Count: 3, Time: day(6)},
784
{Key: []string{"a"}, Prefix: true, Count: 3, Time: day(13)},
787
store.CounterRequest{
794
{Key: []string{"a", "b"}, Prefix: false, Count: 2, Time: day(6)},
795
{Key: []string{"a", "c"}, Prefix: false, Count: 1, Time: day(6)},
796
{Key: []string{"a", "c"}, Prefix: true, Count: 3, Time: day(13)},
801
for _, test := range tests {
802
result, err := s.store.Counters(&test.request)
804
c.Assert(result, DeepEquals, test.result)
808
func (s *TrivialSuite) TestEventString(c *C) {
809
c.Assert(store.EventPublished, Matches, "published")
810
c.Assert(store.EventPublishError, Matches, "publish-error")
811
for kind := store.CharmEventKind(1); kind < store.EventKindCount; kind++ {
812
// This guarantees the switch in String is properly
813
// updated with new event kinds.
814
c.Assert(kind.String(), Matches, "[a-z-]+")