Merge lp:~gary/juju-gui/simplifycharmstore into lp:juju-gui/experimental

Proposed by Gary Poster
Status: Merged
Merged at revision: 206
Proposed branch: lp:~gary/juju-gui/simplifycharmstore
Merge into: lp:juju-gui/experimental
Diff against target: 1329 lines (+365/-462)
14 files modified
app/app.js (+2/-1)
app/models/charm.js (+172/-259)
app/modules.js (+3/-1)
app/store/charm.js (+31/-51)
app/templates/charm-search-result.handlebars (+3/-6)
app/views/charm-search.js (+31/-15)
test/data/search_results.json (+16/-9)
test/data/series_search_results.json (+7/-2)
test/test_app.js (+1/-1)
test/test_charm_configuration.js (+2/-2)
test/test_charm_search.js (+4/-4)
test/test_charm_store.js (+17/-14)
test/test_model.js (+74/-95)
test/test_service_view.js (+2/-2)
To merge this branch: bzr merge lp:~gary/juju-gui/simplifycharmstore
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+131086@code.launchpad.net

Description of the change

change charm store data structures

This change is hopefully the last round of changes, at least for a long while, to the underlying charm store infrastructure. It is more deletes than additions, and changes the code to take advantage of the changes Kapil made to the charm store.

The sorting code is simplified yet again.

https://codereview.appspot.com/6733067/

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Reviewers: mp+131086_code.launchpad.net,

Message:
Please take a look.

Description:
Last change round of charm store data structures

This change is hopefully the last round of changes, at least for a long
while, to the underlying charm store infrastructure. It is more deletes
than additions, and changes the code to take advantage of the changes
Kapil made to the charm store.

I made some decisions as to how to factor some of these things and would
be happy to describe my rationale for changes if desired. I'm thinking
particularly of the immediate creation of charm objects as search
results. That allowed for some simplifications and I think is mostly a
win.

The sorting code is simplified yet again.

Thanks

Gary

https://code.launchpad.net/~gary/juju-gui/simplifycharmstore/+merge/131086

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/6733067/

Affected files:
   A [revision details]
   M app/app.js
   M app/models/charm.js
   M app/modules.js
   M app/store/charm.js
   M app/templates/charm-search-result.handlebars
   M app/views/charm-search.js
   M test/data/search_results.json
   M test/data/series_search_results.json
   M test/test_app.js
   M test/test_charm_configuration.js
   M test/test_charm_search.js
   M test/test_charm_store.js
   M test/test_model.js
   M test/test_service_view.js

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Even though this is a large branch I think it removes more code than it
adds which is always a plus. I had some minor feedback but this LGTM.

I'd still rather Kapil get a chance to look this over but I think the
general architectural issues that were present before (and which I
didn't take into account either) are no longer present.

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js
File app/models/charm.js (right):

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js#newcode56
app/models/charm.js:56: this.on('load', function() { this.loaded = true;
});
Don't you need to either use self here or pass this as context to the
'on' call?

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js#newcode84
app/models/charm.js:84: options.get_charm(
Why the two naming styles on these_twoMethods? get_charm, loadByPath?

https://codereview.appspot.com/6733067/diff/1/app/templates/charm-search-result.handlebars
File app/templates/charm-search-result.handlebars (right):

https://codereview.appspot.com/6733067/diff/1/app/templates/charm-search-result.handlebars#newcode13
app/templates/charm-search-result.handlebars:13: {{#if
owner}}{{owner}}/{{/if}}{{package_name}}</a>
I thought you didn't need the #if when there is no content other than
the var which can default to null, no?

https://codereview.appspot.com/6733067/

Revision history for this message
Gary Poster (gary) wrote :

*** Submitted:

change charm store data structures

This change is hopefully the last round of changes, at least for a long
while, to the underlying charm store infrastructure. It is more deletes
than additions, and changes the code to take advantage of the changes
Kapil made to the charm store.

The sorting code is simplified yet again.

R=benjamin.saller
CC=
https://codereview.appspot.com/6733067

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js
File app/models/charm.js (right):

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js#newcode56
app/models/charm.js:56: this.on('load', function() { this.loaded = true;
});
On 2012/10/26 07:56:19, benjamin.saller wrote:
> Don't you need to either use self here or pass this as context to the
'on' call?
No, because the listener is on "this" so the context is what I want. I
already have a test that verifies, and I doublechecked in the chromium
debugger as well.

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js#newcode84
app/models/charm.js:84: options.get_charm(
On 2012/10/26 07:56:19, benjamin.saller wrote:
> Why the two naming styles on these_twoMethods? get_charm, loadByPath?
As we discussed, it's because we haven't standardized one way or the
other across all our files. Within a file, we are consistent. This
uses get_charm, from the older env js, and loadByPath, from the newer
charm store js. I got your agreement in person that this is fine.
OTOH I switched charmIDRe and idElements in this file, based on your
reminder.

https://codereview.appspot.com/6733067/diff/1/app/templates/charm-search-result.handlebars
File app/templates/charm-search-result.handlebars (right):

https://codereview.appspot.com/6733067/diff/1/app/templates/charm-search-result.handlebars#newcode13
app/templates/charm-search-result.handlebars:13: {{#if
owner}}{{owner}}/{{/if}}{{package_name}}</a>
On 2012/10/26 07:56:19, benjamin.saller wrote:
> I thought you didn't need the #if when there is no content other than
the var
> which can default to null, no?
Yes, but I have the slash after the owner.

https://codereview.appspot.com/6733067/

Revision history for this message
Gary Poster (gary) wrote :

Thank you for the review, Ben. I have comments in-line, largely in line
with what we discussed in person.

You gave me permission to go ahead and land this. Kapil mentioned that
he questioned the use of the Model load/sync code given that it is of
minimal value to our use case, but if I need to address this I will do
it in a subsequent branch.

Thanks

Gary

https://codereview.appspot.com/6733067/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/app.js'
2--- app/app.js 2012-10-19 01:44:45 +0000
3+++ app/app.js 2012-10-23 20:09:21 +0000
4@@ -261,7 +261,8 @@
5 show_unit: function(req) {
6 console.log(
7 'App: Route: Unit', req.params.id, req.path, req.pendingRoutes);
8- var unit_id = req.params.id.replace('-', '/');
9+ // This replacement honors service names that have a hyphen in them.
10+ var unit_id = req.params.id.replace(/^(\S+)-(\d+)$/, '$1/$2');
11 var unit = this.db.units.getById(unit_id);
12 if (unit) {
13 // Once the unit is loaded we need to get the full details of the
14
15=== modified file 'app/models/charm.js'
16--- app/models/charm.js 2012-10-19 01:53:03 +0000
17+++ app/models/charm.js 2012-10-23 20:09:21 +0000
18@@ -2,67 +2,12 @@
19
20 YUI.add('juju-charm-models', function(Y) {
21
22+
23 var models = Y.namespace('juju.models');
24
25- // This is how the charm_id_re regex works for various inputs. The first
26- // element is always the initial string, which we have elided in the
27- // examples.
28- // 'cs:~marcoceppi/precise/word-press-17' ->
29- // [..."cs", "marcoceppi", "precise", "word-press", "17"]
30- // 'cs:~marcoceppi/precise/word-press' ->
31- // [..."cs", "marcoceppi", "precise", "word-press", undefined]
32- // 'cs:precise/word-press' ->
33- // [..."cs", undefined, "precise", "word-press", undefined]
34- // 'cs:precise/word-press-17'
35- // [..."cs", undefined, "precise", "word-press", "17"]
36- var charm_id_re = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)(?:-(\d+))?$/,
37- parse_charm_id = function(id) {
38- var parts = charm_id_re.exec(id),
39- result = {};
40- if (parts) {
41- parts.shift();
42- Y.each(
43- Y.Array.zip(
44- ['scheme', 'owner', 'series', 'package_name', 'revision'],
45- parts),
46- function(pair) { result[pair[0]] = pair[1]; });
47- if (!Y.Lang.isValue(result.scheme)) {
48- result.scheme = 'cs'; // This is the default.
49- }
50- return result;
51- }
52- // return undefined;
53- },
54- _calculate_full_charm_name = function(elements) {
55- var tmp = [elements.series, elements.package_name];
56- if (elements.owner) {
57- tmp.unshift('~' + elements.owner);
58- }
59- return tmp.join('/');
60- },
61- _calculate_charm_store_path = function(elements) {
62- return [(elements.owner ? '~' + elements.owner : 'charms'),
63- elements.series, elements.package_name, 'json'].join('/');
64- },
65- _calculate_base_charm_id = function(elements) {
66- return elements.scheme + ':' + _calculate_full_charm_name(elements);
67- },
68- _reconsititute_charm_id = function(elements) {
69- return _calculate_base_charm_id(elements) + '-' + elements.revision;
70- },
71- _clean_charm_data = function(data) {
72- data.is_subordinate = data.subordinate;
73- Y.each(['subordinate', 'name', 'revision', 'store_revision'],
74- function(nm) { delete data[nm]; });
75- return data;
76- };
77- // This is exposed for testing purposes.
78- models.parse_charm_id = parse_charm_id;
79-
80- // For simplicity and uniformity, there is a single Charm class and a
81- // single CharmList class. Charms, once instantiated and loaded with data
82- // from their respective sources, are immutable and read-only. This reflects
83- // the reality of how we interact with them.
84+ // Charms, once instantiated and loaded with data from their respective
85+ // sources, are immutable and read-only. This reflects the reality of how we
86+ // interact with them.
87
88 // Charm instances can represent both environment charms and charm store
89 // charms. A charm id is reliably and uniquely associated with a given
90@@ -70,127 +15,155 @@
91
92 // Therefore, the database keeps these charms separate in two different
93 // CharmList instances. One is db.charms, representing the environment
94- // charms. The other is maintained by and within the persistent charm panel
95- // instance. As you'd expect, environment charms are what to use when
96- // viewing or manipulating the environment. Charm store charms are what we
97- // can browse to select and deploy new charms to the environment.
98+ // charms. The other, from the charm store, is maintained by and within the
99+ // persistent charm panel instance. As you'd expect, environment charms are
100+ // what to use when viewing or manipulating the environment. Charm store
101+ // charms are what we can browse to select and deploy new charms to the
102+ // environment.
103
104- // Environment charms begin their lives with full charm ids, as provided by
105- // services in the environment:
106+ // Charms begin their lives with full charm ids, as provided by
107+ // services in the environment and the charm store:
108
109 // [SCHEME]:(~[OWNER]/)?[SERIES]/[PACKAGE NAME]-[REVISION].
110
111 // With an id, we can instantiate a charm: typically we use
112- // "db.charms.add({id: [ID]})". Finally, we load the charm's data from the
113- // environment using the standard YUI Model method "load," providing an
114- // object with a get_charm callable, and an optional callback (see YUI
115- // docs). The env has a get_charm method, so, by design, it works nicely:
116- // "charm.load(env, optionalCallback)". The get_charm method is expected to
117- // return what the env version does: either an object with a "result" object
118- // containing the charm data, or an object with an "err" attribute.
119-
120- // The charms in the charm store have a significant difference, beyond the
121- // source of their data: they are addressed in the charm store by a path
122- // that does not include the revision number, and charm store searches do
123- // not include revision numbers in the results. Therefore, we cannot
124- // immediately instantiate a charm, because it requires a full id in order
125- // to maintain the idea of an immutable charm associated with a unique charm
126- // id. However, the charm information that returns does have a revision
127- // number (the most recent); moreover, over time the charm may be updated,
128- // leading to a new charm revision. We model this by creating a new charm.
129-
130- // Since we cannot create or search for charms without a revision number
131- // using the normal methods, the charm list has a couple of helpers for this
132- // story. The workhorse is "loadOneByBaseId". A "base id" is an id without
133- // a revision.
134-
135- // The arguments to "loadOneById" are a base id and a hash of other options.
136- // The hash must have a "charm_store" attribute, that itself loadByPath
137- // method, like the one in app/store/charm.js. It may have zero or more of
138- // the following: a success callback, accepting the fully loaded charm with
139- // the newest revision for the given base id; a failure callback, accepting
140- // the Y.io response object after a failure; and a "force" attribute that,
141- // if it is a Javascript boolean truth-y value, forces a load even if a
142- // charm with the given id already is in the charm list.
143-
144- // "getOneByBaseId" simply returns the charm with the highest revision and
145- // "the given base id from the charm list, without trying to load
146- // "information.
147+ // "db.charms.add({id: [ID]})". Finally, we load the charm's data over the
148+ // network using the standard YUI Model method "load," providing an object
149+ // with a get_charm callable, and an optional callback (see YUI docs). Both
150+ // the env and the charm store have a get_charm method, so, by design, it
151+ // works easily: "charm.load(env, optionalCallback)" or
152+ // "charm.load(charm_store, optionalCallback)". The get_charm method must
153+ // either callback using the default YUI approach for this code, a boolean
154+ // indicating failure, and a result; or it must return what the env version
155+ // does: an object with a "result" object containing the charm data, or an
156+ // object with an "err" attribute.
157
158 // In both cases, environment charms and charm store charms, a charm's
159 // "loaded" attribute is set to true once it has all the data from its
160 // environment.
161
162- var Charm = Y.Base.create('charm', Y.Model, [], {
163- initializer: function() {
164- this.loaded = false;
165- this.on('load', function() { this.loaded = true; });
166- },
167- sync: function(action, options, callback) {
168- if (action !== 'read') {
169- throw (
170- 'Only use the "read" action; "' + action + '" not supported.');
171- }
172- if (!Y.Lang.isValue(options.get_charm)) {
173- throw 'You must supply a get_charm function.';
174- }
175- options.get_charm(
176- this.get('id'),
177- // This is the success callback, or the catch-all callback for
178- // get_charm.
179- function(response) {
180- // Handle the env.get_charm response specially, for ease of use. If
181- // it doesn't match that pattern, pass it through.
182- if (response.err) {
183- callback(true, response);
184- } else if (response.result) {
185- callback(false, response.result);
186- } else { // This would typically be a string.
187- callback(false, response);
188+ var charm_id_re = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+)$/,
189+ id_elements = ['scheme', 'owner', 'series', 'package_name', 'revision'],
190+ Charm = Y.Base.create('charm', Y.Model, [], {
191+ initializer: function() {
192+ var id = this.get('id'),
193+ parts = id && charm_id_re.exec(id),
194+ self = this;
195+ if (!Y.Lang.isValue(id) || !parts) {
196+ throw 'Developers must initialize charms with a well-formed id.';
197+ }
198+ this.loaded = false;
199+ this.on('load', function() { this.loaded = true; });
200+ parts.shift();
201+ Y.each(
202+ Y.Array.zip(id_elements, parts),
203+ function(pair) { self.set(pair[0], pair[1]); });
204+ // full_name
205+ var tmp = [this.get('series'), this.get('package_name')],
206+ owner = this.get('owner');
207+ if (owner) {
208+ tmp.unshift('~' + owner);
209+ }
210+ this.set('full_name', tmp.join('/'));
211+ // charm_store_path
212+ this.set(
213+ 'charm_store_path',
214+ [(owner ? '~' + owner : 'charms'),
215+ this.get('series'),
216+ (this.get('package_name') + '-' + this.get('revision')),
217+ 'json'
218+ ].join('/'));
219+ },
220+ sync: function(action, options, callback) {
221+ if (action !== 'read') {
222+ throw (
223+ 'Only use the "read" action; "' + action + '" not supported.');
224+ }
225+ if (Y.Lang.isValue(options.get_charm)) {
226+ // This is an env.
227+ options.get_charm(
228+ this.get('id'),
229+ function(response) {
230+ if (response.err) {
231+ callback(true, response);
232+ } else if (response.result) {
233+ callback(false, response.result);
234+ } else {
235+ // What's going on? This does not look like either of our
236+ // expected signatures. Declare a loading error.
237+ callback(true, response);
238+ }
239+ }
240+ );
241+ } else if (Y.Lang.isValue(options.loadByPath)) {
242+ // This is a charm store.
243+ options.loadByPath(
244+ this.get('charm_store_path'),
245+ { success: function(response) {
246+ callback(false, response);
247+ },
248+ failure: function(response) {
249+ callback(true, response);
250+ }
251+ });
252+ } else {
253+ throw 'You must supply a get_charm or loadByPath function.';
254+ }
255+ },
256+ parse: function() {
257+ var data = Charm.superclass.parse.apply(this, arguments),
258+ self = this;
259+ data.is_subordinate = data.subordinate;
260+ Y.each(data, function(value, key) {
261+ if (!value ||
262+ !self.attrAdded(key) ||
263+ Y.Lang.isValue(self.get(key))) {
264+ delete data[key];
265 }
266- },
267- // This is the optional error callback.
268- function(response) {
269- callback(true, response);
270- }
271- );
272- },
273- parse: function() {
274- return _clean_charm_data(Charm.superclass.parse.apply(this, arguments));
275- }
276- }, {
277- ATTRS: {
278- id: {
279- lazyAdd: false,
280- setter: function(val) {
281- if (!val) {
282- return val;
283- }
284- var parts = parse_charm_id(val),
285- self = this;
286- parts.revision = parseInt(parts.revision, 10);
287- Y.each(parts, function(value, key) {
288- self._set(key, value);
289 });
290- this._set(
291- 'charm_store_path', _calculate_charm_store_path(parts));
292- this._set('full_name', _calculate_full_charm_name(parts));
293- return _reconsititute_charm_id(parts);
294+ if (data.owner === 'charmers') {
295+ delete data.owner;
296+ }
297+ return data;
298 },
299- validator: function(val) {
300- var parts = parse_charm_id(val);
301- return (parts && Y.Lang.isValue(parts.revision));
302+ compare: function(other, relevance, otherRelevance) {
303+ // Official charms sort before owned charms.
304+ // If !X.owner, that means it is owned by charmers.
305+ var owner = this.get('owner'),
306+ otherOwner = other.get('owner');
307+ if (!owner && otherOwner) {
308+ return -1;
309+ } else if (owner && !otherOwner) {
310+ return 1;
311+ // Relevance is next most important.
312+ } else if (relevance && (relevance !== otherRelevance)) {
313+ // Higher relevance comes first.
314+ return otherRelevance - relevance;
315+ // Otherwise sort by package name, then by owner, then by revision.
316+ } else {
317+ return (
318+ (this.get('package_name').localeCompare(
319+ other.get('package_name'))) ||
320+ (owner ? owner.localeCompare(otherOwner) : 0) ||
321+ (this.get('revision') - other.get('revision')));
322+ }
323 }
324- },
325- // All of the below are loaded except as noted.
326- bzr_branch: {writeOnce: true},
327- charm_store_path: {readOnly: true}, // calculated
328- config: {writeOnce: true},
329- description: {writeOnce: true},
330- full_name: {readOnly: true}, // calculated
331- is_subordinate: {writeOnce: true},
332- last_change:
333- { writeOnce: true,
334+ }, {
335+ ATTRS: {
336+ id: {
337+ writeOnce: true,
338+ validator: function(val) {
339+ return Y.Lang.isString(val) && !!charm_id_re.exec(val);
340+ }
341+ },
342+ bzr_branch: {writeOnce: true},
343+ charm_store_path: {writeOnce: true},
344+ config: {writeOnce: true},
345+ description: {writeOnce: true},
346+ full_name: {writeOnce: true},
347+ is_subordinate: {writeOnce: true},
348+ last_change: {
349+ writeOnce: true,
350 setter: function(val) {
351 // Normalize created value from float to date object.
352 if (val && val.created) {
353@@ -201,102 +174,42 @@
354 return val;
355 }
356 },
357- maintainer: {writeOnce: true},
358- metadata: {writeOnce: true},
359- package_name: {readOnly: true}, // calculated
360- owner: {readOnly: true}, // calculated
361- peers: {writeOnce: true},
362- proof: {writeOnce: true},
363- provides: {writeOnce: true},
364- requires: {writeOnce: true},
365- revision: {readOnly: true}, // calculated
366- scheme: {readOnly: true}, // calculated
367- series: {readOnly: true}, // calculated
368- summary: {writeOnce: true},
369- url: {writeOnce: true}
370- }
371- });
372+ maintainer: {writeOnce: true},
373+ metadata: {writeOnce: true},
374+ package_name: {writeOnce: true},
375+ owner: {writeOnce: true},
376+ peers: {writeOnce: true},
377+ proof: {writeOnce: true},
378+ provides: {writeOnce: true},
379+ requires: {writeOnce: true},
380+ revision: {
381+ writeOnce: true,
382+ setter: function(val) {
383+ if (Y.Lang.isValue(val)) {
384+ val = parseInt(val, 10);
385+ }
386+ return val;
387+ }
388+ },
389+ scheme: {
390+ value: 'cs',
391+ writeOnce: true,
392+ setter: function(val) {
393+ if (!Y.Lang.isValue(val)) {
394+ val = 'cs';
395+ }
396+ return val;
397+ }
398+ },
399+ series: {writeOnce: true},
400+ summary: {writeOnce: true},
401+ url: {writeOnce: true}
402+ }
403+ });
404 models.Charm = Charm;
405
406 var CharmList = Y.Base.create('charmList', Y.ModelList, [], {
407- model: Charm,
408-
409- initializer: function() {
410- this._baseIdHash = {}; // base id (without revision) to array of charms.
411- },
412-
413- _addToBaseIdHash: function(charm) {
414- var baseId = charm.get('scheme') + ':' + charm.get('full_name'),
415- matches = this._baseIdHash[baseId];
416- if (!matches) {
417- matches = this._baseIdHash[baseId] = [];
418- }
419- matches.push(charm);
420- // Note that we don't handle changing baseIds or removed charms because
421- // that should not happen.
422- // Sort on newest charms first.
423- matches.sort(function(a, b) {
424- var revA = parseInt(a.get('revision'), 10),
425- revB = parseInt(b.get('revision'), 10);
426- return revB - revA;
427- });
428- },
429-
430- add: function() {
431- var result = CharmList.superclass.add.apply(this, arguments);
432- if (Y.Lang.isArray(result)) {
433- Y.each(result, this._addToBaseIdHash, this);
434- } else {
435- this._addToBaseIdHash(result);
436- }
437- return result;
438- },
439-
440- getOneByBaseId: function(id) {
441- var match = parse_charm_id(id),
442- baseId = match && _calculate_base_charm_id(match),
443- charms = baseId && this._baseIdHash[baseId];
444- return charms && charms[0];
445- },
446-
447- loadOneByBaseId: function(id, options) {
448- var match = parse_charm_id(id);
449- if (match) {
450- if (!options.force) {
451- var charm = this.getOneByBaseId(_calculate_base_charm_id(match));
452- if (charm) {
453- if (options.success) {
454- options.success(charm);
455- }
456- return;
457- }
458- }
459- var path = _calculate_charm_store_path(match),
460- self = this;
461- options.charm_store.loadByPath(
462- path,
463- { success: function(data) {
464- // We fall back to 0 for revision. Some records do not have one
465- // still in the charm store, such as
466- // http://jujucharms.com/charms/precise/appflower/json (as of this
467- // writing).
468- match.revision = data.store_revision || 0;
469- id = _reconsititute_charm_id(match);
470- charm = self.getById(id);
471- if (!charm) {
472- charm = self.add({id: id});
473- charm.setAttrs(_clean_charm_data(data));
474- charm.loaded = true;
475- }
476- if (options.success) {
477- options.success(charm);
478- }
479- },
480- failure: options.failure });
481- } else {
482- throw id + ' is not a valid base charm id';
483- }
484- }
485+ model: Charm
486 }, {
487 ATTRS: {}
488 });
489
490=== modified file 'app/modules.js'
491--- app/modules.js 2012-10-19 01:58:34 +0000
492+++ app/modules.js 2012-10-23 20:09:21 +0000
493@@ -1,6 +1,6 @@
494 GlobalConfig = {
495 // Uncomment for debug versions of YUI.
496- //filter: 'debug',
497+ filter: 'debug',
498 // Uncomment for verbose logging of YUI
499 debug: false,
500
501@@ -83,6 +83,7 @@
502 },
503
504 'juju-charm-models': {
505+ requires: ['juju-charm-id'],
506 fullpath: '/juju-ui/models/charm.js'
507 },
508
509@@ -103,6 +104,7 @@
510 },
511
512 'juju-charm-store': {
513+ requires: ['juju-charm-id'],
514 fullpath: '/juju-ui/store/charm.js'
515 },
516
517
518=== modified file 'app/store/charm.js'
519--- app/store/charm.js 2012-10-19 01:53:03 +0000
520+++ app/store/charm.js 2012-10-23 20:09:21 +0000
521@@ -16,9 +16,11 @@
522 }
523 });
524 },
525- // The query can be a string that is passed directly to the search url, or a
526- // hash that is marshalled to the correct format (e.g., {series:precise,
527- // owner:charmers}).
528+ // The query can be a string that is passed directly to the search url, or
529+ // a hash that is marshalled to the correct format (e.g.,
530+ // {series:precise owner:charmers}). The method returns CharmId instances
531+ // grouped by series and ordered within the groups according to the
532+ // CharmId compare function.
533 find: function(query, options) {
534 if (!Y.Lang.isString(query)) {
535 var tmp = [];
536@@ -38,32 +40,33 @@
537 console.log('results update', result_set);
538 options.success(
539 this._normalizeCharms(
540- result_set.results, options.defaultSeries));
541+ result_set.results, options.list, options.defaultSeries));
542 }, this),
543 'failure': options.failure
544 }});
545 },
546- // Stash the base id on each charm, convert the official "charmers" owner to
547- // an empty owner, and group the charms within series. The series are
548- // arranged with first the defaultSeries, if any, and then all other
549- // available series arranged from newest to oldest. Within each series,
550- // official charms come first, sorted by relevance if available and package
551- // name otherwise; and then owned charms follow, sorted again by relevance
552- // if available and package name otherwise.
553- _normalizeCharms: function(charms, defaultSeries) {
554- var hash = {};
555- Y.each(charms, function(charm) {
556- charm.baseId = charm.series + '/' + charm.name;
557- if (charm.owner === 'charmers') {
558- charm.owner = null;
559- } else {
560- charm.baseId = '~' + charm.owner + '/' + charm.baseId;
561- }
562- charm.baseId = 'cs:' + charm.baseId;
563- if (!Y.Lang.isValue(hash[charm.series])) {
564- hash[charm.series] = [];
565- }
566- hash[charm.series].push(charm);
567+ // Convert the charm data into Charm instances, using only id and
568+ // relevance. Group them into series. The series are arranged with first
569+ // the defaultSeries, if any, and then all other available series arranged
570+ // from newest to oldest. Within each series, official charms come first,
571+ // sorted by relevance if available and package name otherwise; and then
572+ // owned charms follow, sorted again by relevance if available and package
573+ // name otherwise.
574+ _normalizeCharms: function(results, list, defaultSeries) {
575+ var hash = {},
576+ relevances = {};
577+ Y.each(results, function(result) {
578+ var charm = list.getById(result.store_url);
579+ if (!charm) {
580+ charm = list.add(
581+ { id: result.store_url, summary: result.summary });
582+ }
583+ var series = charm.get('series');
584+ if (!Y.Lang.isValue(hash[series])) {
585+ hash[series] = [];
586+ }
587+ hash[series].push(charm);
588+ relevances[charm.get('id')] = result.relevance;
589 });
590 var series_names = Y.Object.keys(hash);
591 series_names.sort(function(a, b) {
592@@ -71,42 +74,19 @@
593 return -1;
594 } else if (a !== defaultSeries && b === defaultSeries) {
595 return 1;
596- } else if (a > b) {
597- return -1;
598- } else if (a < b) {
599- return 1;
600 } else {
601- return 0;
602+ return -a.localeCompare(b);
603 }
604 });
605 return Y.Array.map(series_names, function(name) {
606 var charms = hash[name];
607 charms.sort(function(a, b) {
608- // If !a.owner, that means it is owned by charmers.
609- if (!a.owner && b.owner) {
610- return -1;
611- } else if (a.owner && !b.owner) {
612- return 1;
613- } else if (a.relevance < b.relevance) {
614- return 1; // Higher relevance comes first.
615- } else if (a.relevance > b.relevance) {
616- return -1;
617- } else if (a.name < b.name) {
618- return -1;
619- } else if (a.name > b.name) {
620- return 1;
621- } else if (a.owner < b.owner) {
622- return -1;
623- } else if (a.owner > b.owner) {
624- return 1;
625- } else {
626- return 0;
627- }
628+ return a.compare(
629+ b, relevances[a.get('id')], relevances[b.get('id')]);
630 });
631 return {series: name, charms: hash[name]};
632 });
633 }
634-
635 }, {
636 ATTRS: {
637 datasource: {
638
639=== modified file 'app/templates/charm-search-result.handlebars'
640--- app/templates/charm-search-result.handlebars 2012-10-19 01:44:45 +0000
641+++ app/templates/charm-search-result.handlebars 2012-10-23 20:09:21 +0000
642@@ -7,14 +7,11 @@
643 {{#charms}}
644 <li class="charm-entry">
645 <div>
646-
647 <button class="btn btn-primary deploy"
648- data-url="{{baseId}}">Deploy</button>
649- <a class="charm-detail" href="{{baseId}}">
650- {{#if owner}}{{owner}}/{{/if}}{{name}}</a>
651-
652+ data-url="{{id}}">Deploy</button>
653+ <a class="charm-detail" href="{{id}}">
654+ {{#if owner}}{{owner}}/{{/if}}{{package_name}}</a>
655 <div class="charm-summary">{{summary}}</div>
656-
657 </div>
658 </li>
659 {{/charms}}
660
661=== modified file 'app/views/charm-search.js'
662--- app/views/charm-search.js 2012-10-19 01:53:03 +0000
663+++ app/views/charm-search.js 2012-10-23 20:09:21 +0000
664@@ -50,7 +50,8 @@
665 self.set('resultEntries', charms);
666 },
667 failure: Y.bind(this._showErrors, this),
668- defaultSeries: this.get('defaultSeries')
669+ defaultSeries: this.get('defaultSeries'),
670+ list: this.get('charms')
671 });
672 }
673 });
674@@ -63,7 +64,8 @@
675 self.set('defaultEntries', charms);
676 },
677 failure: Y.bind(this._showErrors, this),
678- defaultSeries: this.get('defaultSeries')
679+ defaultSeries: this.get('defaultSeries'),
680+ list: this.get('charms')
681 });
682 }
683 });
684@@ -81,8 +83,17 @@
685 searchText = this.get('searchText'),
686 defaultEntries = this.get('defaultEntries'),
687 resultEntries = this.get('resultEntries'),
688- entries = searchText ? resultEntries : defaultEntries;
689- container.setHTML(this.template({charms: entries}));
690+ raw_entries = searchText ? resultEntries : defaultEntries,
691+ entries = raw_entries && raw_entries.map(
692+ function(data) {
693+ return {
694+ series: data.series,
695+ charms: data.charms.map(
696+ function(charm) { return charm.getAttrs(); })
697+ };
698+ }
699+ );
700+ container.setHTML(this.template({ charms: entries }));
701 return this;
702 },
703 showDetails: function(ev) {
704@@ -156,7 +167,7 @@
705 views.CharmDescriptionView = CharmDescriptionView;
706
707 var CharmConfigurationView = Y.Base.create(
708- 'CharmCollectionView', Y.View, [views.JujuBaseView], {
709+ 'CharmConfigurationView', Y.View, [views.JujuBaseView], {
710 template: views.Templates['charm-pre-configuration'],
711 tooltip: null,
712 configFileContent: null,
713@@ -399,7 +410,8 @@
714 charmsSearchPanel = new CharmCollectionView(
715 { container: charmsSearchPanelNode,
716 app: app,
717- charmStore: charmStore }),
718+ charmStore: charmStore,
719+ charms: charms }),
720 descriptionPanelNode = Y.Node.create(),
721 descriptionPanel = new CharmDescriptionView(
722 { container: descriptionPanelNode,
723@@ -432,15 +444,19 @@
724 contentNode.append(panels[config.name].get('container'));
725 if (config.charmId) {
726 newPanel.set('model', null); // Clear out the old.
727- charms.loadOneByBaseId(
728- config.charmId,
729- { success: function(charm) {newPanel.set('model', charm);},
730- failure: function(data) {
731- console.log('error loading charm', data);
732- newPanel.fire('changePanel', {name: 'charms'});
733- },
734- charm_store: charmStore
735- });
736+ var charm = charms.getById(config.charmId);
737+ if (charm.loaded) {
738+ newPanel.set('model', charm);
739+ } else {
740+ charm.load(charmStore, function(err, response) {
741+ if (err) {
742+ console.log('error loading charm', response);
743+ newPanel.fire('changePanel', {name: 'charms'});
744+ } else {
745+ newPanel.set('model', charm);
746+ }
747+ });
748+ }
749 } else { // This is the search panel.
750 newPanel.render();
751 }
752
753=== modified file 'test/data/search_results.json'
754--- test/data/search_results.json 2012-10-19 01:44:45 +0000
755+++ test/data/search_results.json 2012-10-23 20:09:21 +0000
756@@ -1,63 +1,70 @@
757 {
758 "matches": 7,
759- "charm_total": 466,
760+ "charm_total": 469,
761 "results_size": 7,
762- "search_time": 0.0011610984802246094,
763+ "search_time": 0.001168966293334961,
764 "results": [
765 {
766 "data_url": "/charms/precise/cassandra/json",
767 "name": "cassandra",
768+ "store_url": "cs:precise/cassandra-2",
769 "series": "precise",
770 "summary": "distributed storage system for structured data",
771- "relevance": 29.552706339212623,
772+ "relevance": 29.58930787782692,
773 "owner": "charmers"
774 },
775 {
776 "data_url": "/charms/oneiric/cassandra/json",
777 "name": "cassandra",
778+ "store_url": "cs:~charmers/oneiric/cassandra-0",
779 "series": "oneiric",
780 "summary": "distributed storage system for structured data",
781- "relevance": 29.429741180715094,
782+ "relevance": 29.46580425464922,
783 "owner": "charmers"
784 },
785 {
786 "data_url": "/~jjo/precise/cassandra/json",
787 "name": "cassandra",
788+ "store_url": "cs:~jjo/precise/cassandra-12",
789 "series": "precise",
790 "summary": "distributed storage system for structured data",
791- "relevance": 28.060048153642214,
792+ "relevance": 28.090375175505876,
793 "owner": "jjo"
794 },
795 {
796 "data_url": "/~ev/precise/errors/json",
797 "name": "errors",
798+ "store_url": "cs:~ev/precise/errors-0",
799 "series": "precise",
800 "summary": "https://errors.ubuntu.com",
801- "relevance": 13.168754948011127,
802+ "relevance": 13.18957931651269,
803 "owner": "ev"
804 },
805 {
806 "data_url": "/~ev/precise/daisy/json",
807 "name": "daisy",
808+ "store_url": "cs:~ev/precise/daisy-15",
809 "series": "precise",
810 "summary": "Daisy error reporting server",
811- "relevance": 12.721871518319244,
812+ "relevance": 12.767880577035612,
813 "owner": "ev"
814 },
815 {
816 "data_url": "/~ev/precise/daisy-retracer/json",
817 "name": "daisy-retracer",
818+ "store_url": "cs:~ev/precise/daisy-retracer-8",
819 "series": "precise",
820 "summary": "Daisy error reporting server retracer",
821- "relevance": 12.662102672474589,
822+ "relevance": 12.539732674900428,
823 "owner": "ev"
824 },
825 {
826 "data_url": "/~negronjl/oneiric/cassandra/json",
827 "name": "cassandra",
828+ "store_url": "cs:~negronjl/oneiric/cassandra-0",
829 "series": "oneiric",
830 "summary": "distributed storage system for structured data",
831- "relevance": 30.40622159052724,
832+ "relevance": 30.451103781408868,
833 "owner": "negronjl"
834 }
835 ]
836
837=== modified file 'test/data/series_search_results.json'
838--- test/data/series_search_results.json 2012-10-19 01:44:45 +0000
839+++ test/data/series_search_results.json 2012-10-23 20:09:21 +0000
840@@ -1,12 +1,13 @@
841 {
842 "matches": 5,
843- "charm_total": 466,
844+ "charm_total": 469,
845 "results_size": 5,
846- "search_time": 0.0008029937744140625,
847+ "search_time": 0.0007879734039306641,
848 "results": [
849 {
850 "data_url": "/charms/quantal/glance/json",
851 "name": "glance",
852+ "store_url": "cs:~charmers/quantal/glance-2",
853 "series": "quantal",
854 "summary": "OpenStack Image Registry and Delivery Service",
855 "relevance": 0.0,
856@@ -15,6 +16,7 @@
857 {
858 "data_url": "/charms/quantal/nova-cloud-controller/json",
859 "name": "nova-cloud-controller",
860+ "store_url": "cs:~charmers/quantal/nova-cloud-controller-1",
861 "series": "quantal",
862 "summary": "Openstack nova controller node.",
863 "relevance": 0.0,
864@@ -23,6 +25,7 @@
865 {
866 "data_url": "/charms/quantal/nova-volume/json",
867 "name": "nova-volume",
868+ "store_url": "cs:~charmers/quantal/nova-volume-0",
869 "series": "quantal",
870 "summary": "OpenStack Compute - storage",
871 "relevance": 0.0,
872@@ -31,6 +34,7 @@
873 {
874 "data_url": "/charms/quantal/nova-compute/json",
875 "name": "nova-compute",
876+ "store_url": "cs:~charmers/quantal/nova-compute-1",
877 "series": "quantal",
878 "summary": "OpenStack compute",
879 "relevance": 0.0,
880@@ -39,6 +43,7 @@
881 {
882 "data_url": "/charms/quantal/nyancat/json",
883 "name": "nyancat",
884+ "store_url": "cs:~charmers/quantal/nyancat-0",
885 "series": "quantal",
886 "summary": "Nyancat telnet server",
887 "relevance": 0.0,
888
889=== modified file 'test/test_app.js'
890--- test/test_app.js 2012-10-19 01:53:03 +0000
891+++ test/test_app.js 2012-10-23 20:09:21 +0000
892@@ -88,7 +88,7 @@
893
894 // charms also require a mapping but only a name, not a function
895 app.getModelURL(wp_charm).should.equal(
896- '/charms/charms/precise/wordpress/json');
897+ '/charms/charms/precise/wordpress-6/json');
898 });
899
900 it('should display the configured environment name', function() {
901
902=== modified file 'test/test_charm_configuration.js'
903--- test/test_charm_configuration.js 2012-10-20 18:17:23 +0000
904+++ test/test_charm_configuration.js 2012-10-23 20:09:21 +0000
905@@ -91,7 +91,7 @@
906 received_charm_url = charm_url;
907 received_service_name = service_name;
908 }},
909- charm = new models.Charm({id: 'precise/mysql-7'}),
910+ charm = new models.Charm({id: 'cs:precise/mysql-7'}),
911 view = new views.CharmConfigurationView(
912 { container: container,
913 model: charm,
914@@ -119,7 +119,7 @@
915 received_config = config;
916 received_num_units = num_units;
917 }},
918- charm = new models.Charm({id: 'precise/mysql-7'}),
919+ charm = new models.Charm({id: 'cs:precise/mysql-7'}),
920 view = new views.CharmConfigurationView(
921 { container: container,
922 model: charm,
923
924=== modified file 'test/test_charm_search.js'
925--- test/test_charm_search.js 2012-10-19 01:53:03 +0000
926+++ test/test_charm_search.js 2012-10-23 20:09:21 +0000
927@@ -5,7 +5,7 @@
928 searchResult = '{"results": [{"data_url": "this is my URL", ' +
929 '"name": "membase", "series": "precise", "summary": ' +
930 '"Membase Server", "relevance": 8.728194117350437, ' +
931- '"owner": "charmers"}]}';
932+ '"owner": "charmers", "store_url": "cs:precise/membase-6"}]}';
933
934 before(function(done) {
935 Y = YUI(GlobalConfig).use(
936@@ -85,7 +85,7 @@
937
938 searchTriggered.should.equal(true);
939 node.one('.charm-entry .btn.deploy').getData('url').should.equal(
940- 'cs:precise/membase');
941+ 'cs:precise/membase-6');
942 });
943
944 it('must be able to trigger charm details', function() {
945@@ -107,7 +107,7 @@
946 testing: true
947 }),
948 node = panel.node;
949- db.charms.add({id: 'cs:precise/membase'});
950+ db.charms.add({id: 'cs:precise/membase-6'});
951
952 panel.show();
953 var field = Y.one('#charm-search-field');
954@@ -138,7 +138,7 @@
955 testing: true
956 }),
957 node = panel.node,
958- charm = db.charms.add({id: 'cs:precise/membase'});
959+ charm = db.charms.add({id: 'cs:precise/membase-6'});
960 charm.loaded = true;
961 panel.show();
962 var field = Y.one('#charm-search-field');
963
964=== modified file 'test/test_charm_store.js'
965--- test/test_charm_store.js 2012-10-19 01:53:03 +0000
966+++ test/test_charm_store.js 2012-10-23 20:09:21 +0000
967@@ -8,9 +8,10 @@
968 before(function(done) {
969 Y = YUI(GlobalConfig).use(
970 'datasource-local', 'json-stringify', 'juju-charm-store',
971- 'datasource-io', 'io', 'array-extras',
972+ 'datasource-io', 'io', 'array-extras', 'juju-charm-models',
973 function(Y) {
974 juju = Y.namespace('juju');
975+ models = Y.namespace('juju.models');
976 done();
977 });
978 });
979@@ -108,19 +109,20 @@
980 results.length.should.equal(2);
981 results[0].series.should.equal('precise');
982 Y.Array.map(results[0].charms, function(charm) {
983- return charm.owner;
984- }).should.eql([null, 'jjo', 'ev', 'ev', 'ev']);
985+ return charm.get('owner');
986+ }).should.eql([undefined, 'jjo', 'ev', 'ev', 'ev']);
987 Y.Array.map(results[0].charms, function(charm) {
988- return charm.baseId;
989+ return charm.get('id');
990 }).should.eql([
991- 'cs:precise/cassandra',
992- 'cs:~jjo/precise/cassandra',
993- 'cs:~ev/precise/errors',
994- 'cs:~ev/precise/daisy',
995- 'cs:~ev/precise/daisy-retracer']);
996+ 'cs:precise/cassandra-2',
997+ 'cs:~jjo/precise/cassandra-12',
998+ 'cs:~ev/precise/errors-0',
999+ 'cs:~ev/precise/daisy-15',
1000+ 'cs:~ev/precise/daisy-retracer-8']);
1001 done();
1002 },
1003- failure: assert.fail
1004+ failure: assert.fail,
1005+ list: new models.CharmList()
1006 });
1007 });
1008
1009@@ -135,7 +137,8 @@
1010 results[0].series.should.equal('oneiric');
1011 done();
1012 },
1013- failure: assert.fail
1014+ failure: assert.fail,
1015+ list: new models.CharmList()
1016 });
1017 });
1018
1019@@ -149,7 +152,7 @@
1020 results.length.should.equal(1);
1021 results[0].series.should.equal('quantal');
1022 Y.Array.map(results[0].charms, function(charm) {
1023- return charm.name;
1024+ return charm.get('package_name');
1025 }).should.eql([
1026 'glance',
1027 'nova-cloud-controller',
1028@@ -158,9 +161,9 @@
1029 'nyancat']);
1030 done();
1031 },
1032- failure: assert.fail
1033+ failure: assert.fail,
1034+ list: new models.CharmList()
1035 });
1036 });
1037-
1038 });
1039 })();
1040
1041=== modified file 'test/test_model.js'
1042--- test/test_model.js 2012-10-19 01:53:03 +0000
1043+++ test/test_model.js 2012-10-23 20:09:21 +0000
1044@@ -19,56 +19,19 @@
1045 var _ = expect(charm.get('owner')).to.not.exist;
1046 charm.get('full_name').should.equal('precise/openstack-dashboard');
1047 charm.get('charm_store_path').should.equal(
1048- 'charms/precise/openstack-dashboard/json');
1049+ 'charms/precise/openstack-dashboard-0/json');
1050 });
1051
1052 it('must convert timestamps into time objects', function() {
1053 var time = 1349797266.032,
1054 date = new Date(time),
1055 charm = new models.Charm(
1056- { id: 'precise/foo', last_change: {created: time / 1000} });
1057+ { id: 'cs:precise/foo-9', last_change: {created: time / 1000} });
1058 charm.get('last_change').created.should.eql(date);
1059 });
1060
1061 });
1062
1063- describe('charm id helper functions', function() {
1064- var Y, models;
1065-
1066- before(function(done) {
1067- Y = YUI(GlobalConfig).use('juju-models', function(Y) {
1068- models = Y.namespace('juju.models');
1069- done();
1070- });
1071- });
1072-
1073- it('must parse fully qualified names', function() {
1074- // undefined never equals undefined.
1075- var res = models.parse_charm_id('cs:precise/openstack-dashboard-0');
1076- res.scheme.should.equal('cs');
1077- var _ = expect(res.owner).to.not.exist;
1078- res.series.should.equal('precise');
1079- res.package_name.should.equal('openstack-dashboard');
1080- res.revision.should.equal('0');
1081- });
1082-
1083- it('must parse names without revisions', function() {
1084- var res = models.parse_charm_id('cs:precise/openstack-dashboard'),
1085- _ = expect(res.revision).to.not.exist;
1086- });
1087-
1088- it('must parse fully qualified names with owners', function() {
1089- models.parse_charm_id('cs:~bac/precise/openstack-dashboard-0').owner
1090- .should.equal('bac');
1091- });
1092-
1093- it('must parse fully qualified names with hyphenated owners', function() {
1094- models.parse_charm_id('cs:~alt-bac/precise/openstack-dashboard-0').owner
1095- .should.equal('alt-bac');
1096- });
1097-
1098- });
1099-
1100 describe('juju models', function() {
1101 var Y, models;
1102
1103@@ -81,15 +44,16 @@
1104
1105 it('must be able to create charm', function() {
1106 var charm = new models.Charm(
1107- {id: 'cs:~bac/precise/openstack-dashboard-0'});
1108+ {id: 'cs:~alt-bac/precise/openstack-dashboard-0'});
1109 charm.get('scheme').should.equal('cs');
1110- charm.get('owner').should.equal('bac');
1111+ charm.get('owner').should.equal('alt-bac');
1112 charm.get('series').should.equal('precise');
1113 charm.get('package_name').should.equal('openstack-dashboard');
1114 charm.get('revision').should.equal(0);
1115- charm.get('full_name').should.equal('~bac/precise/openstack-dashboard');
1116+ charm.get('full_name').should.equal(
1117+ '~alt-bac/precise/openstack-dashboard');
1118 charm.get('charm_store_path').should.equal(
1119- '~bac/precise/openstack-dashboard/json');
1120+ '~alt-bac/precise/openstack-dashboard-0/json');
1121 });
1122
1123 it('must be able to parse real-world charm names', function() {
1124@@ -97,7 +61,12 @@
1125 charm.get('full_name').should.equal('precise/openstack-dashboard');
1126 charm.get('package_name').should.equal('openstack-dashboard');
1127 charm.get('charm_store_path').should.equal(
1128- 'charms/precise/openstack-dashboard/json');
1129+ 'charms/precise/openstack-dashboard-0/json');
1130+ charm.get('scheme').should.equal('cs');
1131+ var _ = expect(charm.get('owner')).to.not.exist;
1132+ charm.get('series').should.equal('precise');
1133+ charm.get('package_name').should.equal('openstack-dashboard');
1134+ charm.get('revision').should.equal(0);
1135 });
1136
1137 it('must be able to parse individually owned charms', function() {
1138@@ -108,14 +77,28 @@
1139 charm.get('full_name').should.equal('~marco-ceppi/precise/wordpress');
1140 charm.get('package_name').should.equal('wordpress');
1141 charm.get('charm_store_path').should.equal(
1142- '~marco-ceppi/precise/wordpress/json');
1143+ '~marco-ceppi/precise/wordpress-17/json');
1144+ charm.get('revision').should.equal(17);
1145 });
1146
1147 it('must reject bad charm ids.', function() {
1148- var charm = new models.Charm({id: 'foobar'});
1149- var _ = expect(charm.get('id')).to.not.exist;
1150- charm.set('id', 'barfoo');
1151- _ = expect(charm.get('id')).to.not.exist;
1152+ try {
1153+ var charm = new models.Charm({id: 'foobar'});
1154+ assert.fail('Should have thrown an error');
1155+ } catch (e) {
1156+ e.should.equal(
1157+ 'Developers must initialize charms with a well-formed id.');
1158+ }
1159+ });
1160+
1161+ it('must reject missing charm ids at initialization.', function() {
1162+ try {
1163+ var charm = new models.Charm();
1164+ assert.fail('Should have thrown an error');
1165+ } catch (e) {
1166+ e.should.equal(
1167+ 'Developers must initialize charms with a well-formed id.');
1168+ }
1169 });
1170
1171 it('must be able to create charm list', function() {
1172@@ -403,7 +386,7 @@
1173 });
1174
1175 it('will throw an exception with non-read sync', function() {
1176- var charm = new models.Charm({id: 'local:precise/foo'});
1177+ var charm = new models.Charm({id: 'local:precise/foo-4'});
1178 try {
1179 charm.sync('create');
1180 assert.fail('Should have thrown an error');
1181@@ -424,39 +407,40 @@
1182 }
1183 });
1184
1185- it('throws an error if you do not pass a get_charm function', function() {
1186- var charm = new models.Charm({id: 'local:precise/foo'});
1187- try {
1188- charm.sync('read', {});
1189- assert.fail('Should have thrown an error');
1190- } catch (e) {
1191- e.should.equal(
1192- 'You must supply a get_charm function.');
1193- }
1194- try {
1195- charm.sync('read', {env: 42});
1196- assert.fail('Should have thrown an error');
1197- } catch (e) {
1198- e.should.equal(
1199- 'You must supply a get_charm function.');
1200- }
1201- try {
1202- charm.sync('read', {charm_store: 42});
1203- assert.fail('Should have thrown an error');
1204- } catch (e) {
1205- e.should.equal(
1206- 'You must supply a get_charm function.');
1207- }
1208- });
1209+ it('throws an error if you do not pass get_charm or loadByPath function',
1210+ function() {
1211+ var charm = new models.Charm({id: 'local:precise/foo-4'});
1212+ try {
1213+ charm.sync('read', {});
1214+ assert.fail('Should have thrown an error');
1215+ } catch (e) {
1216+ e.should.equal(
1217+ 'You must supply a get_charm or loadByPath function.');
1218+ }
1219+ try {
1220+ charm.sync('read', {env: 42});
1221+ assert.fail('Should have thrown an error');
1222+ } catch (e) {
1223+ e.should.equal(
1224+ 'You must supply a get_charm or loadByPath function.');
1225+ }
1226+ try {
1227+ charm.sync('read', {charm_store: 42});
1228+ assert.fail('Should have thrown an error');
1229+ } catch (e) {
1230+ e.should.equal(
1231+ 'You must supply a get_charm or loadByPath function.');
1232+ }
1233+ });
1234
1235 it('must send request to juju environment for local charms', function() {
1236- var charm = new models.Charm({id: 'local:precise/foo'}).load(env);
1237+ var charm = new models.Charm({id: 'local:precise/foo-4'}).load(env);
1238 assert(!charm.loaded);
1239 conn.last_message().op.should.equal('get_charm');
1240 });
1241
1242 it('must handle success from local charm request', function(done) {
1243- var charm = new models.Charm({id: 'local:precise/foo'}).load(
1244+ var charm = new models.Charm({id: 'local:precise/foo-4'}).load(
1245 env,
1246 function(err, response) {
1247 assert(!err);
1248@@ -471,7 +455,7 @@
1249 });
1250
1251 it('must handle failure from local charm request', function(done) {
1252- var charm = new models.Charm({id: 'local:precise/foo'}).load(
1253+ var charm = new models.Charm({id: 'local:precise/foo-4'}).load(
1254 env,
1255 function(err, response) {
1256 assert(err);
1257@@ -490,22 +474,18 @@
1258 { responseText: Y.JSON.stringify(
1259 { summary: 'wowza', subordinate: true, store_revision: 7 })});
1260
1261- var list = new models.CharmList();
1262- list.loadOneByBaseId(
1263- 'cs:precise/foo',
1264- { success: function(charm) {
1265+ var charm = new models.Charm({id: 'cs:precise/foo-7'});
1266+ charm.load(
1267+ charm_store,
1268+ function(err, data) {
1269+ if (err) { assert.fail('should succeed!'); }
1270 assert(charm.loaded);
1271 charm.get('summary').should.equal('wowza');
1272 charm.get('is_subordinate').should.equal(true);
1273 charm.get('scheme').should.equal('cs');
1274 charm.get('revision').should.equal(7);
1275 charm.get('id').should.equal('cs:precise/foo-7');
1276- list.getById('cs:precise/foo-7').should.equal(charm);
1277- list.getOneByBaseId('cs:precise/foo').should.equal(charm);
1278 done();
1279- },
1280- failure: assert.fail,
1281- charm_store: charm_store
1282 });
1283 });
1284
1285@@ -521,15 +501,14 @@
1286 original.apply(datasource, [e]);
1287 };
1288 data.push({responseText: Y.JSON.stringify({darn_it: 'uh oh!'})});
1289- list.loadOneByBaseId(
1290- 'cs:precise/foo',
1291- { success: assert.fail,
1292- failure: function(err) {
1293- var _ = expect(list.getOneByBaseId('cs:precise/foo'))
1294- .to.not.exist;
1295- done();
1296- },
1297- charm_store: charm_store
1298+ var charm = new models.Charm({id: 'cs:precise/foo-7'});
1299+ charm.load(
1300+ charm_store,
1301+ function(err, data) {
1302+ if (!err) {
1303+ assert.fail('should fail!');
1304+ }
1305+ done();
1306 });
1307 });
1308
1309
1310=== modified file 'test/test_service_view.js'
1311--- test/test_service_view.js 2012-10-12 18:27:36 +0000
1312+++ test/test_service_view.js 2012-10-23 20:09:21 +0000
1313@@ -30,7 +30,7 @@
1314 app = { env: env, db: db,
1315 getModelURL: function(model, intent) {
1316 return model.get('name'); }};
1317- charm = new models.Charm({id: 'cs:precise/mysql',
1318+ charm = new models.Charm({id: 'cs:precise/mysql-5',
1319 description: 'A DB'});
1320 db.charms.add([charm]);
1321 // Add units sorted by id as that is what we expect from the server.
1322@@ -40,7 +40,7 @@
1323 ]);
1324 service = new models.Service({
1325 id: 'mysql',
1326- charm: 'cs:precise/mysql',
1327+ charm: 'cs:precise/mysql-5',
1328 unit_count: db.units.size(),
1329 exposed: false});
1330

Subscribers

People subscribed via source and target branches