Merge lp:~benji/juju-gui/bug-1088507 into lp:juju-gui/experimental

Proposed by Benji York
Status: Merged
Merged at revision: 285
Proposed branch: lp:~benji/juju-gui/bug-1088507
Merge into: lp:juju-gui/experimental
Diff against target: 925 lines (+433/-395)
7 files modified
Makefile (+19/-10)
app/app.js (+1/-0)
app/views/charm-panel.js (+32/-4)
bin/merge-files (+2/-0)
test/index.html (+5/-1)
test/test_charm_configuration.js (+20/-29)
test/test_model.js (+354/-351)
To merge this branch: bzr merge lp:~benji/juju-gui/bug-1088507
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+140020@code.launchpad.net

Description of the change

Make the prod tests pass.

Changes a test that was failing because of some slight style difference to be
more direct. Some slight style differences still exist between prod and dev.

The charm model module (app/models/charm.js) is now included in the combined
and minified JS file.

The event-simulate and node-event-simulate functions are needed for the tests,
so they are now directly loaded by test/index.html.

The test_charm_configuration.js file needs juju-charm-models but that module
was not specified.

test/test_model.js had to be rearranged so the YUI().use method was called
inside the "before" method instead of around the entire test module. This
resulted in a mass reindenting.

Incidentally: Fixes the Makefile so YUI module assets are in the right place
so requests for them do not 404.

https://codereview.appspot.com/6947057/

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

Land with changes.

Thank you for accomplishing this difficult task.

In addition to the comments below, please add a request in process.rst
that reviewers run both prod tests and debug tests. Then, in the
Makefile, either make the default test the prod test, or make it thumb
its nose at us and tell us to run one of the other targets.

Gary

https://codereview.appspot.com/6947057/diff/1/Makefile
File Makefile (right):

https://codereview.appspot.com/6947057/diff/1/Makefile#newcode293
Makefile:293: cp -r --parents */assets
"$(PWD)/build-$(1)/juju-ui/assets/")
Looks nicer to me, thanks.

https://codereview.appspot.com/6947057/diff/1/bin/merge-files
File bin/merge-files (right):

https://codereview.appspot.com/6947057/diff/1/bin/merge-files#newcode50
bin/merge-files:50: paths.push(syspath.join(process.cwd(),
'app/models/charm.js'));
As we discussed, please either figure out why readdir is not finding
charm.js, or put an XXX and a bug, and we will come back to this.

https://codereview.appspot.com/6947057/diff/1/test/index.html
File test/index.html (right):

https://codereview.appspot.com/6947057/diff/1/test/index.html#newcode9
test/index.html:9: <script
src="/juju-ui/assets/node-event-simulate.js"></script>
You explained the horror behind these two inclusions. Please add a
comment so others can enjoy.

https://codereview.appspot.com/6947057/

Revision history for this message
Nicola Larosa (teknico) wrote :

Needs fixing.

Running "make test-prod" produces 60 failures here, which is admittedly
a nice improvement down from 79, but still not enough. :-)

You most likely don't see these failures: I'd like to understand why I
do.

There's one more thing I'd like to clarify, see comment inline.

https://codereview.appspot.com/6947057/diff/1/Makefile
File Makefile (right):

https://codereview.appspot.com/6947057/diff/1/Makefile#newcode293
Makefile:293: cp -r --parents */assets
"$(PWD)/build-$(1)/juju-ui/assets/")
There's been some back and forth on this. You originally put in the "cp"
instruction; then I wanted to have symlinks in the build directories
rather than copies, so I spent some effort to come up with the "find"
command that created the right links in the right places, and did not
see requests for them return 404. Your experience has apparently been
different. I'd like to understand: 1) why; 2) if there's value enough,
in having symlinks instead of copies, to worry about this.

https://codereview.appspot.com/6947057/

lp:~benji/juju-gui/bug-1088507 updated
278. By Benji York

Review change: remove the "test" target and have it tell the user to be explicit about what they want

279. By Benji York

add more commentary from reveis

280. By Benji York

add comment suggested in review

281. By Benji York

add some test tiles to the build directories

Revision history for this message
Nicola Larosa (teknico) wrote :

It works now, thanks. Do not worry about the other issue, we'll worry
about it later.

Land this.

https://codereview.appspot.com/6947057/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2012-12-14 18:04:28 +0000
3+++ Makefile 2012-12-17 14:57:47 +0000
4@@ -284,15 +284,20 @@
5 ln -sf "$(PWD)/build/juju-ui/assets/sprite.png" build-$(1)/juju-ui/assets/
6 ln -sf "$(PWD)/node_modules/yui/assets/skins/sam/rail-x.png" \
7 build-$(1)/juju-ui/assets/combined-css/rail-x.png
8- # Link each YUI module's assets.
9- mkdir -p build-$(1)/juju-ui/assets/skins/night/ \
10- build-$(1)/juju-ui/assets/skins/sam/
11- find node_modules/yui/ -path "*/skins/night/*" -type f \
12- -exec ln -sf "$(PWD)/{}" build-$(1)/juju-ui/assets/skins/night/ \;
13- find node_modules/yui/ -path "*/skins/sam/*" -type f \
14- -exec ln -sf "$(PWD)/{}" build-$(1)/juju-ui/assets/skins/sam/ \;
15- find node_modules/yui/ -path "*/assets/*" \! -path "*/skins/*" -type f \
16- -exec ln -sf "$(PWD)/{}" build-$(1)/juju-ui/assets/ \;
17+ ln -sf "$(PWD)/node_modules/yui/event-simulate/event-simulate.js" \
18+ build-$(1)/juju-ui/assets/
19+ ln -sf "$(PWD)/node_modules/yui/node-event-simulate/node-event-simulate.js" \
20+ build-$(1)/juju-ui/assets
21+ # Copy each YUI module's assets to a parallel directory in the build
22+ # location. This is run in a subshell (indicated by the parenthesis)
23+ # so we can change directory and have it not effect this process. To
24+ # understand how it does what it does look at the man page for cp,
25+ # particularly "--parents". Notice that this makes copies instead of
26+ # links. This goes against the way the dependencies are structured and
27+ # so may be a problem in the future. If so, a way to do this as links
28+ # would be called for.
29+ (cd node_modules/yui/ && \
30+ cp -r --parents */assets "$(PWD)/build-$(1)/juju-ui/assets/")
31 endef
32
33 $(LINK_DEBUG_FILES):
34@@ -319,7 +324,11 @@
35 test-prod: build-prod
36 ./test-server.sh prod
37
38-test: test-debug
39+test:
40+ @echo "Deprecated. Please run either 'make test-prod' or 'make"
41+ @echo "test-debug', to test the production or debug environments"
42+ @echo "respectively. Run 'make help' to list the main available "
43+ @echo "targets."
44
45 server:
46 @echo "Deprecated. Please run either 'make prod' or 'make debug',"
47
48=== modified file 'app/app.js'
49--- app/app.js 2012-12-04 22:08:40 +0000
50+++ app/app.js 2012-12-17 14:57:47 +0000
51@@ -761,6 +761,7 @@
52 }, '0.5.2', {
53 requires: [
54 'juju-models',
55+ 'juju-charm-models',
56 'juju-views',
57 'juju-controllers',
58 'juju-view-charm-search',
59
60=== modified file 'app/views/charm-panel.js'
61--- app/views/charm-panel.js 2012-12-03 22:24:16 +0000
62+++ app/views/charm-panel.js 2012-12-17 14:57:47 +0000
63@@ -697,6 +697,31 @@
64 'input.config-field[type=checkbox]':
65 {click: function(evt) {evt.target.focus();}}
66 },
67+ /**
68+ * Determines the Y coordinate that would center a tooltip on a field.
69+ *
70+ * @static
71+ * @param {Number} fieldY The current Y position of the tooltip.
72+ * @param {Number} fieldHeight The hight of the field.
73+ * @param {Number} tooltipHeight The height of the tooltip.
74+ * @return {Number} New Y coordinate for the tooltip.
75+ */
76+ _calculateTooltipY: function(fieldY, fieldHeight, tooltipHeight) {
77+ var y_offset = (tooltipHeight - fieldHeight) / 2;
78+ return fieldY - y_offset;
79+ },
80+ /**
81+ * Determines the X coordinate that would place a tooltip next to a
82+ * field.
83+ *
84+ * @static
85+ * @param {Number} fieldX The current X position of the tooltip.
86+ * @param {Number} tooltipWidth The width of the tooltip.
87+ * @return {Number} New X coordinate for the tooltip.
88+ */
89+ _calculateTooltipX: function(fieldX, tooltipWidth) {
90+ return fieldX - tooltipWidth - 15;
91+ },
92 _moveTooltip: function() {
93 if (this.tooltip.field &&
94 Y.DOM.inRegion(
95@@ -708,10 +733,13 @@
96 var widget = this.tooltip.get('boundingBox'),
97 tooltipWidth = widget.get('clientWidth'),
98 tooltipHeight = widget.get('clientHeight'),
99- y_offset = (tooltipHeight - fieldHeight) / 2;
100- this.tooltip.move( // These are the x, y coordinates.
101- [this.tooltip.panel.getX() - tooltipWidth - 15,
102- this.tooltip.field.getY() - y_offset]);
103+ fieldX = this.tooltip.panel.getX(),
104+ fieldY = this.tooltip.field.getY(),
105+ tooltipX = this._calculateTooltipX(
106+ fieldX, tooltipWidth),
107+ tooltipY = this._calculateTooltipY(
108+ fieldY, fieldHeight, tooltipHeight);
109+ this.tooltip.move([tooltipX, tooltipY]);
110 if (!this.tooltip.get('visible')) {
111 this.tooltip.show();
112 }
113
114=== modified file 'bin/merge-files'
115--- bin/merge-files 2012-12-14 20:39:08 +0000
116+++ bin/merge-files 2012-12-17 14:57:47 +0000
117@@ -48,6 +48,8 @@
118
119 // templates.js is a generated file. It is not part of the app directory.
120 paths.push(syspath.join(process.cwd(), 'build/juju-ui/templates.js'));
121+ // XXX Why do we have to do this? (bug 1090563).
122+ paths.push(syspath.join(process.cwd(), 'app/models/charm.js'));
123
124 merge.combineJs(paths, 'build/juju-ui/assets/app.js');
125
126
127=== modified file 'test/index.html'
128--- test/index.html 2012-12-11 04:11:39 +0000
129+++ test/index.html 2012-12-17 14:57:47 +0000
130@@ -3,8 +3,12 @@
131 <head>
132 <meta charset="utf-8">
133 <link rel="stylesheet" href="assets/mocha.css">
134+ <script src="/juju-ui/assets/modules.js"></script>
135 <script src="/juju-ui/assets/all-yui.js"></script>
136- <script src="/juju-ui/assets/modules.js"></script>
137+ <!-- Since only the tests depend on these files and the prod tests disable
138+ the YUI loader, we have to include them manually here. -->
139+ <script src="/juju-ui/assets/event-simulate.js"></script>
140+ <script src="/juju-ui/assets/node-event-simulate.js"></script>
141 <script src="assets/chai.js"></script>
142 <script src="assets/mocha.js"></script>
143 <script src="utils.js"></script>
144
145=== modified file 'test/test_charm_configuration.js'
146--- test/test_charm_configuration.js 2012-10-29 10:01:29 +0000
147+++ test/test_charm_configuration.js 2012-12-17 14:57:47 +0000
148@@ -24,6 +24,7 @@
149 before(function(done) {
150 Y = YUI(GlobalConfig).use(
151 'juju-models',
152+ 'juju-charm-models',
153 'juju-views',
154 'juju-gui',
155 'juju-env',
156@@ -199,35 +200,25 @@
157 tooltip.get('visible').should.equal(true);
158 });
159
160- it('must keep the tooltip aligned with its field', function() {
161- var charm = new models.Charm({id: 'precise/mysql-7'}),
162- view = new views.CharmConfigurationView(
163- { container: container,
164- model: charm,
165- tooltipDelay: 0 });
166- charm.setAttrs(charmConfig);
167- charm.loaded = true;
168- view.render();
169- var tooltip = view.tooltip,
170- controls = container.all('.control-group input'),
171- panel = container.one('.charm-panel');
172- // The panel needs to be scrollable and smaller than what it contains. We
173- // do this by setting a height to the panel and then setting the height to
174- // one of the controls to something much bigger.
175- panel.setStyles({height: '400px', overflowY: 'auto'});
176- controls.item(2).set('height', '4000px');
177- // We need to have the field visible or else the call to "focus" will
178- // change the positioning after our calculation has occurred, thus
179- // changing our Y field.
180- controls.item(1).scrollIntoView();
181- controls.item(1).focus();
182- var originalY = tooltip.get('boundingBox').getY();
183- panel.set('scrollTop', panel.get('scrollTop') + 10);
184- // The simulate module does not support firing scroll events so we call
185- // the associated method directly.
186- view._moveTooltip();
187- Math.floor(tooltip.get('boundingBox').getY())
188- .should.equal(Math.floor(originalY - 10));
189+ it('must keep the tooltip aligned with its field vertically', function() {
190+ // The tooltip's Y coordinate should be such that it is centered vertically
191+ // on its associated field.
192+ var fieldHeight = 7;
193+ var tooltipHeight = 17;
194+ var fieldY = 1000;
195+ var view = new views.CharmConfigurationView();
196+ var y = view._calculateTooltipY(fieldY, fieldHeight, tooltipHeight);
197+ assert.equal(y, 995);
198+ });
199+
200+ it('must keep the tooltip to the left of its field', function() {
201+ // The tooltip's X coordinate should be such that it is to the left of its
202+ // associated field.
203+ var tooltipWidth = 100;
204+ var fieldX = 1000;
205+ var view = new views.CharmConfigurationView();
206+ var x = view._calculateTooltipX(fieldX, tooltipWidth);
207+ assert.equal(x, 885);
208 });
209
210 it('must hide the tooltip when its field scrolls away', function() {
211
212=== modified file 'test/test_model.js'
213--- test/test_model.js 2012-11-23 16:21:32 +0000
214+++ test/test_model.js 2012-12-17 14:57:47 +0000
215@@ -1,356 +1,359 @@
216 'use strict';
217
218-YUI(GlobalConfig).use('juju-models', function(Y) {
219- describe('charm normalization', function() {
220- var models;
221-
222- before(function() {
223- models = Y.namespace('juju.models');
224- });
225-
226- it('must create derived attributes from official charm id', function() {
227- var charm = new models.Charm(
228- {id: 'cs:precise/openstack-dashboard-0'});
229- charm.get('scheme').should.equal('cs');
230- var _ = expect(charm.get('owner')).to.not.exist;
231- charm.get('full_name').should.equal('precise/openstack-dashboard');
232- charm.get('charm_store_path').should.equal(
233- 'charms/precise/openstack-dashboard-0/json');
234- });
235-
236- it('must convert timestamps into time objects', function() {
237- var time = 1349797266.032,
238- date = new Date(time),
239- charm = new models.Charm(
240- { id: 'cs:precise/foo-9', last_change: {created: time / 1000} });
241- charm.get('last_change').created.should.eql(date);
242- });
243-
244- });
245-});
246-
247-YUI(GlobalConfig).use('juju-models', function(Y) {
248- describe('juju models', function() {
249- var models;
250-
251- before(function() {
252- models = Y.namespace('juju.models');
253- });
254-
255- it('must be able to create charm', function() {
256- var charm = new models.Charm(
257- {id: 'cs:~alt-bac/precise/openstack-dashboard-0'});
258- charm.get('scheme').should.equal('cs');
259- charm.get('owner').should.equal('alt-bac');
260- charm.get('series').should.equal('precise');
261- charm.get('package_name').should.equal('openstack-dashboard');
262- charm.get('revision').should.equal(0);
263- charm.get('full_name').should.equal(
264- '~alt-bac/precise/openstack-dashboard');
265- charm.get('charm_store_path').should.equal(
266- '~alt-bac/precise/openstack-dashboard-0/json');
267- });
268-
269- it('must be able to parse real-world charm names', function() {
270- var charm = new models.Charm({id: 'cs:precise/openstack-dashboard-0'});
271- charm.get('full_name').should.equal('precise/openstack-dashboard');
272- charm.get('package_name').should.equal('openstack-dashboard');
273- charm.get('charm_store_path').should.equal(
274- 'charms/precise/openstack-dashboard-0/json');
275- charm.get('scheme').should.equal('cs');
276- var _ = expect(charm.get('owner')).to.not.exist;
277- charm.get('series').should.equal('precise');
278- charm.get('package_name').should.equal('openstack-dashboard');
279- charm.get('revision').should.equal(0);
280- });
281-
282- it('must be able to parse individually owned charms', function() {
283- // Note that an earlier version of the parsing code did not handle
284- // hyphens in user names, so this test intentionally includes one.
285- var charm = new models.Charm(
286- {id: 'cs:~marco-ceppi/precise/wordpress-17'});
287- charm.get('full_name').should.equal('~marco-ceppi/precise/wordpress');
288- charm.get('package_name').should.equal('wordpress');
289- charm.get('charm_store_path').should.equal(
290- '~marco-ceppi/precise/wordpress-17/json');
291- charm.get('revision').should.equal(17);
292- });
293-
294- it('must reject bad charm ids.', function() {
295- try {
296- var charm = new models.Charm({id: 'foobar'});
297- assert.fail('Should have thrown an error');
298- } catch (e) {
299- e.should.equal(
300- 'Developers must initialize charms with a well-formed id.');
301- }
302- });
303-
304- it('must reject missing charm ids at initialization.', function() {
305- try {
306- var charm = new models.Charm();
307- assert.fail('Should have thrown an error');
308- } catch (e) {
309- e.should.equal(
310- 'Developers must initialize charms with a well-formed id.');
311- }
312- });
313-
314- it('must be able to create charm list', function() {
315- var c1 = new models.Charm(
316- { id: 'cs:precise/mysql-2',
317- description: 'A DB'}),
318- c2 = new models.Charm(
319- { id: 'cs:precise/logger-3',
320- description: 'Log sub'}),
321- clist = new models.CharmList();
322- clist.add([c1, c2]);
323- var names = clist.map(function(c) {return c.get('package_name');});
324- names[0].should.equal('mysql');
325- names[1].should.equal('logger');
326- });
327-
328-
329- it('service unit list should be able to get units of a given service',
330- function() {
331- var sl = new models.ServiceList();
332- var sul = new models.ServiceUnitList();
333- var mysql = new models.Service({id: 'mysql'});
334- var wordpress = new models.Service({id: 'wordpress'});
335- sl.add([mysql, wordpress]);
336- sl.getById('mysql').should.equal(mysql);
337- sl.getById('wordpress').should.equal(wordpress);
338-
339- sul.add([{id: 'mysql/0'}, {id: 'mysql/1'}]);
340-
341- var wp0 = {id: 'wordpress/0'},
342- wp1 = {id: 'wordpress/1'};
343- sul.add([wp0, wp1]);
344- wp0.service.should.equal('wordpress');
345-
346- sul.get_units_for_service(mysql, true).getAttrs(['id']).id.should.eql(
347- ['mysql/0', 'mysql/1']);
348- sul.get_units_for_service(wordpress, true).getAttrs(
349- ['id']).id.should.eql(['wordpress/0', 'wordpress/1']);
350- });
351-
352- it('service unit list should be able to aggregate unit statuses',
353- function() {
354- var sl = new models.ServiceList();
355- var sul = new models.ServiceUnitList();
356- var mysql = new models.Service({id: 'mysql'});
357- var wordpress = new models.Service({id: 'wordpress'});
358- sl.add([mysql, wordpress]);
359-
360- var my0 = new models.ServiceUnit(
361- {id: 'mysql/0', agent_state: 'pending'}),
362- my1 = new models.ServiceUnit(
363- {id: 'mysql/1', agent_state: 'pending'});
364-
365- sul.add([my0, my1]);
366-
367- var wp0 = new models.ServiceUnit(
368- { id: 'wordpress/0',
369- agent_state: 'pending'}),
370- wp1 = new models.ServiceUnit(
371- { id: 'wordpress/1',
372- agent_state: 'error'});
373- sul.add([wp0, wp1]);
374-
375- sul.get_informative_states_for_service(mysql).should.eql(
376- {'pending': 2});
377- sul.get_informative_states_for_service(wordpress).should.eql(
378- {'pending': 1, 'error': 1});
379- });
380-
381- it('service unit objects should parse the service name from unit id',
382- function() {
383- var service_unit = {id: 'mysql/0'},
384- db = new models.Database();
385- db.units.add(service_unit);
386- service_unit.service.should.equal('mysql');
387- });
388-
389- it('service unit objects should report their number correctly',
390- function() {
391- var service_unit = {id: 'mysql/5'},
392- db = new models.Database();
393- db.units.add(service_unit);
394- service_unit.number.should.equal(5);
395- });
396-
397- it('must be able to resolve models by modelId', function() {
398- var db = new models.Database();
399-
400- db.services.add([{id: 'wordpress'}, {id: 'mediawiki'}]);
401- db.units.add([{id: 'wordpress/0'}, {id: 'wordpress/1'}]);
402-
403- var model = db.services.item(0);
404- // Single parameter calling
405- db.getModelById([model.name, model.get('id')])
406- .get('id').should.equal('wordpress');
407- // Two parameter interface
408- db.getModelById(model.name, model.get('id'))
409- .get('id').should.equal('wordpress');
410-
411- var unit = db.units.item(0);
412- db.getModelById([unit.name, unit.id]).id.should.equal('wordpress/0');
413- db.getModelById(unit.name, unit.id).id.should.equal('wordpress/0');
414- });
415-
416- it('on_delta should handle remove changes correctly',
417- function() {
418- var db = new models.Database();
419- var my0 = new models.ServiceUnit({id: 'mysql/0',
420- agent_state: 'pending'}),
421- my1 = new models.ServiceUnit({id: 'mysql/1',
422- agent_state: 'pending'});
423- db.units.add([my0, my1]);
424- db.on_delta({data: {result: [
425- ['unit', 'remove', 'mysql/1']
426- ]}});
427- var names = db.units.get('id');
428- names.length.should.equal(1);
429- names[0].should.equal('mysql/0');
430- });
431-
432- it('on_delta should be able to reuse existing services with add',
433- function() {
434- var db = new models.Database();
435- var my0 = new models.Service({id: 'mysql', exposed: true});
436- db.services.add([my0]);
437- // Note that exposed is not set explicitly to false.
438- db.on_delta({data: {result: [
439- ['service', 'add', {id: 'mysql'}]
440- ]}});
441- my0.get('exposed').should.equal(false);
442- });
443-
444- it('on_delta should be able to reuse existing units with add',
445- // Units are special because they use the LazyModelList.
446- function() {
447- var db = new models.Database();
448- var my0 = {id: 'mysql/0', agent_state: 'pending'};
449- db.units.add([my0]);
450- db.on_delta({data: {result: [
451- ['unit', 'add', {id: 'mysql/0', agent_state: 'another'}]
452- ]}});
453- my0.agent_state.should.equal('another');
454- });
455-
456- it('on_delta should reset relation_errors',
457- function() {
458- var db = new models.Database();
459- var my0 = {id: 'mysql/0', relation_errors: {'cache': ['memcached']}};
460- db.units.add([my0]);
461- // Note that relation_errors is not set.
462- db.on_delta({data: {result: [
463- ['unit', 'change', {id: 'mysql/0'}]
464- ]}});
465- my0.relation_errors.should.eql({});
466- });
467-
468- it('ServiceUnitList should accept a list of units at instantiation and ' +
469- 'decorate them', function() {
470- var mysql = new models.Service({id: 'mysql'});
471- var objs = [{id: 'mysql/0'},
472- {id: 'mysql/1'}];
473- var sul = new models.ServiceUnitList({items: objs});
474- var unit_data = sul.get_units_for_service(
475- mysql, true).getAttrs(['service', 'number']);
476- unit_data.service.should.eql(['mysql', 'mysql']);
477- unit_data.number.should.eql([0, 1]);
478- });
479-
480- it('RelationList.has_relations.. should return true if rel found.',
481- function() {
482- var db = new models.Database(),
483- service = new models.Service({id: 'mysql', exposed: false}),
484- rel0 = new models.Relation({
485- id: 'relation-0',
486- endpoints: [
487- ['mediawiki', {name: 'cache', role: 'source'}],
488- ['squid', {name: 'cache', role: 'front'}]],
489- 'interface': 'cache'
490- }),
491- rel1 = new models.Relation({
492- id: 'relation-4',
493- endpoints: [
494- ['something', {name: 'foo', role: 'bar'}],
495- ['mysql', {name: 'la', role: 'lee'}]],
496- 'interface': 'thing'
497- });
498- db.relations.add([rel0, rel1]);
499- db.relations.has_relation_for_endpoint(
500- {service: 'squid', name: 'cache', type: 'cache'}
501- ).should.equal(true);
502- db.relations.has_relation_for_endpoint(
503- {service: 'mysql', name: 'la', type: 'thing'}
504- ).should.equal(true);
505- db.relations.has_relation_for_endpoint(
506- {service: 'squid', name: 'cache', type: 'http'}
507- ).should.equal(false);
508-
509- // We can also pass a service name which must match for the
510- // same relation.
511-
512- db.relations.has_relation_for_endpoint(
513- {service: 'squid', name: 'cache', type: 'cache'},
514- 'kafka'
515- ).should.equal(false);
516-
517- db.relations.has_relation_for_endpoint(
518- {service: 'squid', name: 'cache', type: 'cache'},
519- 'mediawiki'
520- ).should.equal(true);
521-
522- });
523-
524- it('RelationList.get_relations_for_service should do what it says',
525- function() {
526- var db = new models.Database(),
527- service = new models.Service({id: 'mysql', exposed: false}),
528- rel0 = new models.Relation(
529- { id: 'relation-0',
530- endpoints:
531- [['mediawiki', {name: 'cache', role: 'source'}],
532- ['squid', {name: 'cache', role: 'front'}]],
533- 'interface': 'cache'
534- }),
535- rel1 = new models.Relation(
536- { id: 'relation-1',
537- endpoints:
538- [['wordpress', {role: 'peer', name: 'loadbalancer'}]],
539- 'interface': 'reversenginx'
540- }),
541- rel2 = new models.Relation(
542- { id: 'relation-2',
543- endpoints:
544- [['mysql', {name: 'db', role: 'db'}],
545- ['mediawiki', {name: 'storage', role: 'app'}]],
546- 'interface': 'db'
547- }),
548- rel3 = new models.Relation(
549- { id: 'relation-3',
550- endpoints:
551- [['mysql', {role: 'peer', name: 'loadbalancer'}]],
552- 'interface': 'mysql-loadbalancer'
553- }),
554- rel4 = new models.Relation(
555- { id: 'relation-4',
556- endpoints:
557- [['something', {name: 'foo', role: 'bar'}],
558- ['mysql', {name: 'la', role: 'lee'}]],
559- 'interface': 'thing'
560- });
561- db.relations.add([rel0, rel1, rel2, rel3, rel4]);
562- Y.Array.map(
563- db.relations.get_relations_for_service(service),
564- function(r) { return r.get('id'); })
565- .should.eql(['relation-2', 'relation-3', 'relation-4']);
566- });
567- });
568-});
569+describe('charm normalization', function() {
570+ var models;
571+
572+ before(function() {
573+ YUI(GlobalConfig).use('juju-models', 'juju-charm-models', function(Y) {
574+ models = Y.namespace('juju.models');
575+ });
576+ });
577+
578+ it('must create derived attributes from official charm id', function() {
579+ var charm = new models.Charm(
580+ {id: 'cs:precise/openstack-dashboard-0'});
581+ charm.get('scheme').should.equal('cs');
582+ var _ = expect(charm.get('owner')).to.not.exist;
583+ charm.get('full_name').should.equal('precise/openstack-dashboard');
584+ charm.get('charm_store_path').should.equal(
585+ 'charms/precise/openstack-dashboard-0/json');
586+ });
587+
588+ it('must convert timestamps into time objects', function() {
589+ var time = 1349797266.032,
590+ date = new Date(time),
591+ charm = new models.Charm(
592+ { id: 'cs:precise/foo-9', last_change: {created: time / 1000} });
593+ charm.get('last_change').created.should.eql(date);
594+ });
595+
596+});
597+
598+describe('juju models', function() {
599+ var models;
600+
601+ before(function(done) {
602+ YUI(GlobalConfig).use('juju-models', 'juju-charm-models', function(Y) {
603+ models = Y.namespace('juju.models');
604+ done();
605+ });
606+ });
607+
608+ it('must be able to create charm', function() {
609+ var charm = new models.Charm(
610+ {id: 'cs:~alt-bac/precise/openstack-dashboard-0'});
611+ charm.get('scheme').should.equal('cs');
612+ charm.get('owner').should.equal('alt-bac');
613+ charm.get('series').should.equal('precise');
614+ charm.get('package_name').should.equal('openstack-dashboard');
615+ charm.get('revision').should.equal(0);
616+ charm.get('full_name').should.equal(
617+ '~alt-bac/precise/openstack-dashboard');
618+ charm.get('charm_store_path').should.equal(
619+ '~alt-bac/precise/openstack-dashboard-0/json');
620+ });
621+
622+ it('must be able to parse real-world charm names', function() {
623+ var charm = new models.Charm({id: 'cs:precise/openstack-dashboard-0'});
624+ charm.get('full_name').should.equal('precise/openstack-dashboard');
625+ charm.get('package_name').should.equal('openstack-dashboard');
626+ charm.get('charm_store_path').should.equal(
627+ 'charms/precise/openstack-dashboard-0/json');
628+ charm.get('scheme').should.equal('cs');
629+ var _ = expect(charm.get('owner')).to.not.exist;
630+ charm.get('series').should.equal('precise');
631+ charm.get('package_name').should.equal('openstack-dashboard');
632+ charm.get('revision').should.equal(0);
633+ });
634+
635+ it('must be able to parse individually owned charms', function() {
636+ // Note that an earlier version of the parsing code did not handle
637+ // hyphens in user names, so this test intentionally includes one.
638+ var charm = new models.Charm(
639+ {id: 'cs:~marco-ceppi/precise/wordpress-17'});
640+ charm.get('full_name').should.equal('~marco-ceppi/precise/wordpress');
641+ charm.get('package_name').should.equal('wordpress');
642+ charm.get('charm_store_path').should.equal(
643+ '~marco-ceppi/precise/wordpress-17/json');
644+ charm.get('revision').should.equal(17);
645+ });
646+
647+ it('must reject bad charm ids.', function() {
648+ try {
649+ var charm = new models.Charm({id: 'foobar'});
650+ assert.fail('Should have thrown an error');
651+ } catch (e) {
652+ e.should.equal(
653+ 'Developers must initialize charms with a well-formed id.');
654+ }
655+ });
656+
657+ it('must reject missing charm ids at initialization.', function() {
658+ try {
659+ var charm = new models.Charm();
660+ assert.fail('Should have thrown an error');
661+ } catch (e) {
662+ e.should.equal(
663+ 'Developers must initialize charms with a well-formed id.');
664+ }
665+ });
666+
667+ it('must be able to create charm list', function() {
668+ var c1 = new models.Charm(
669+ { id: 'cs:precise/mysql-2',
670+ description: 'A DB'}),
671+ c2 = new models.Charm(
672+ { id: 'cs:precise/logger-3',
673+ description: 'Log sub'}),
674+ clist = new models.CharmList();
675+ clist.add([c1, c2]);
676+ var names = clist.map(function(c) {return c.get('package_name');});
677+ names[0].should.equal('mysql');
678+ names[1].should.equal('logger');
679+ });
680+
681+
682+ it('service unit list should be able to get units of a given service',
683+ function() {
684+ var sl = new models.ServiceList();
685+ var sul = new models.ServiceUnitList();
686+ var mysql = new models.Service({id: 'mysql'});
687+ var wordpress = new models.Service({id: 'wordpress'});
688+ sl.add([mysql, wordpress]);
689+ sl.getById('mysql').should.equal(mysql);
690+ sl.getById('wordpress').should.equal(wordpress);
691+
692+ sul.add([{id: 'mysql/0'}, {id: 'mysql/1'}]);
693+
694+ var wp0 = {id: 'wordpress/0'};
695+ var wp1 = {id: 'wordpress/1'};
696+ sul.add([wp0, wp1]);
697+ wp0.service.should.equal('wordpress');
698+
699+ sul.get_units_for_service(mysql, true).getAttrs(['id']).id.should.eql(
700+ ['mysql/0', 'mysql/1']);
701+ sul.get_units_for_service(wordpress, true).getAttrs(
702+ ['id']).id.should.eql(['wordpress/0', 'wordpress/1']);
703+ });
704+
705+ it('service unit list should be able to aggregate unit statuses',
706+ function() {
707+ var sl = new models.ServiceList();
708+ var sul = new models.ServiceUnitList();
709+ var mysql = new models.Service({id: 'mysql'});
710+ var wordpress = new models.Service({id: 'wordpress'});
711+ sl.add([mysql, wordpress]);
712+
713+ var my0 = new models.ServiceUnit({
714+ id: 'mysql/0',
715+ agent_state: 'pending'});
716+ var my1 = new models.ServiceUnit({
717+ id: 'mysql/1',
718+ agent_state: 'pending'});
719+
720+ sul.add([my0, my1]);
721+
722+ var wp0 = new models.ServiceUnit({
723+ id: 'wordpress/0',
724+ agent_state: 'pending'});
725+ var wp1 = new models.ServiceUnit({
726+ id: 'wordpress/1',
727+ agent_state: 'error'});
728+ sul.add([wp0, wp1]);
729+
730+ sul.get_informative_states_for_service(mysql).should.eql(
731+ {'pending': 2});
732+ sul.get_informative_states_for_service(wordpress).should.eql(
733+ {'pending': 1, 'error': 1});
734+ });
735+
736+ it('service unit objects should parse the service name from unit id',
737+ function() {
738+ var service_unit = {id: 'mysql/0'};
739+ var db = new models.Database();
740+ db.units.add(service_unit);
741+ service_unit.service.should.equal('mysql');
742+ });
743+
744+ it('service unit objects should report their number correctly',
745+ function() {
746+ var service_unit = {id: 'mysql/5'};
747+ var db = new models.Database();
748+ db.units.add(service_unit);
749+ service_unit.number.should.equal(5);
750+ });
751+
752+ it('must be able to resolve models by modelId', function() {
753+ var db = new models.Database();
754+
755+ db.services.add([{id: 'wordpress'}, {id: 'mediawiki'}]);
756+ db.units.add([{id: 'wordpress/0'}, {id: 'wordpress/1'}]);
757+
758+ var model = db.services.item(0);
759+ // Single parameter calling
760+ db.getModelById([model.name, model.get('id')])
761+ .get('id').should.equal('wordpress');
762+ // Two parameter interface
763+ db.getModelById(model.name, model.get('id'))
764+ .get('id').should.equal('wordpress');
765+
766+ var unit = db.units.item(0);
767+ db.getModelById([unit.name, unit.id]).id.should.equal('wordpress/0');
768+ db.getModelById(unit.name, unit.id).id.should.equal('wordpress/0');
769+ });
770+
771+ it('on_delta should handle remove changes correctly',
772+ function() {
773+ var db = new models.Database();
774+ var my0 = new models.ServiceUnit({id: 'mysql/0',
775+ agent_state: 'pending'});
776+ var my1 = new models.ServiceUnit({id: 'mysql/1',
777+ agent_state: 'pending'});
778+ db.units.add([my0, my1]);
779+ db.on_delta({data: {result: [
780+ ['unit', 'remove', 'mysql/1']
781+ ]}});
782+ var names = db.units.get('id');
783+ names.length.should.equal(1);
784+ names[0].should.equal('mysql/0');
785+ });
786+
787+ it('on_delta should be able to reuse existing services with add',
788+ function() {
789+ var db = new models.Database();
790+ var my0 = new models.Service({id: 'mysql', exposed: true});
791+ db.services.add([my0]);
792+ // Note that exposed is not set explicitly to false.
793+ db.on_delta({data: {result: [
794+ ['service', 'add', {id: 'mysql'}]
795+ ]}});
796+ my0.get('exposed').should.equal(false);
797+ });
798+
799+ it('on_delta should be able to reuse existing units with add',
800+ // Units are special because they use the LazyModelList.
801+ function() {
802+ var db = new models.Database();
803+ var my0 = {id: 'mysql/0', agent_state: 'pending'};
804+ db.units.add([my0]);
805+ db.on_delta({data: {result: [
806+ ['unit', 'add', {id: 'mysql/0', agent_state: 'another'}]
807+ ]}});
808+ my0.agent_state.should.equal('another');
809+ });
810+
811+ it('on_delta should reset relation_errors',
812+ function() {
813+ var db = new models.Database();
814+ var my0 = {id: 'mysql/0', relation_errors: {'cache': ['memcached']}};
815+ db.units.add([my0]);
816+ // Note that relation_errors is not set.
817+ db.on_delta({data: {result: [
818+ ['unit', 'change', {id: 'mysql/0'}]
819+ ]}});
820+ my0.relation_errors.should.eql({});
821+ });
822+
823+ it('ServiceUnitList should accept a list of units at instantiation and ' +
824+ 'decorate them', function() {
825+ var mysql = new models.Service({id: 'mysql'});
826+ var objs = [{id: 'mysql/0'},
827+ {id: 'mysql/1'}];
828+ var sul = new models.ServiceUnitList({items: objs});
829+ var unit_data = sul.get_units_for_service(
830+ mysql, true).getAttrs(['service', 'number']);
831+ unit_data.service.should.eql(['mysql', 'mysql']);
832+ unit_data.number.should.eql([0, 1]);
833+ });
834+
835+ it('RelationList.has_relations.. should return true if rel found.',
836+ function() {
837+ var db = new models.Database(),
838+ service = new models.Service({id: 'mysql', exposed: false}),
839+ rel0 = new models.Relation({
840+ id: 'relation-0',
841+ endpoints: [
842+ ['mediawiki', {name: 'cache', role: 'source'}],
843+ ['squid', {name: 'cache', role: 'front'}]],
844+ 'interface': 'cache'
845+ }),
846+ rel1 = new models.Relation({
847+ id: 'relation-4',
848+ endpoints: [
849+ ['something', {name: 'foo', role: 'bar'}],
850+ ['mysql', {name: 'la', role: 'lee'}]],
851+ 'interface': 'thing'
852+ });
853+ db.relations.add([rel0, rel1]);
854+ db.relations.has_relation_for_endpoint(
855+ {service: 'squid', name: 'cache', type: 'cache'}
856+ ).should.equal(true);
857+ db.relations.has_relation_for_endpoint(
858+ {service: 'mysql', name: 'la', type: 'thing'}
859+ ).should.equal(true);
860+ db.relations.has_relation_for_endpoint(
861+ {service: 'squid', name: 'cache', type: 'http'}
862+ ).should.equal(false);
863+
864+ // We can also pass a service name which must match for the
865+ // same relation.
866+
867+ db.relations.has_relation_for_endpoint(
868+ {service: 'squid', name: 'cache', type: 'cache'},
869+ 'kafka'
870+ ).should.equal(false);
871+
872+ db.relations.has_relation_for_endpoint(
873+ {service: 'squid', name: 'cache', type: 'cache'},
874+ 'mediawiki'
875+ ).should.equal(true);
876+
877+ });
878+
879+ it('RelationList.get_relations_for_service should do what it says',
880+ function() {
881+ var db = new models.Database(),
882+ service = new models.Service({id: 'mysql', exposed: false}),
883+ rel0 = new models.Relation(
884+ { id: 'relation-0',
885+ endpoints:
886+ [['mediawiki', {name: 'cache', role: 'source'}],
887+ ['squid', {name: 'cache', role: 'front'}]],
888+ 'interface': 'cache'
889+ }),
890+ rel1 = new models.Relation(
891+ { id: 'relation-1',
892+ endpoints:
893+ [['wordpress', {role: 'peer', name: 'loadbalancer'}]],
894+ 'interface': 'reversenginx'
895+ }),
896+ rel2 = new models.Relation(
897+ { id: 'relation-2',
898+ endpoints:
899+ [['mysql', {name: 'db', role: 'db'}],
900+ ['mediawiki', {name: 'storage', role: 'app'}]],
901+ 'interface': 'db'
902+ }),
903+ rel3 = new models.Relation(
904+ { id: 'relation-3',
905+ endpoints:
906+ [['mysql', {role: 'peer', name: 'loadbalancer'}]],
907+ 'interface': 'mysql-loadbalancer'
908+ }),
909+ rel4 = new models.Relation(
910+ { id: 'relation-4',
911+ endpoints:
912+ [['something', {name: 'foo', role: 'bar'}],
913+ ['mysql', {name: 'la', role: 'lee'}]],
914+ 'interface': 'thing'
915+ });
916+ db.relations.add([rel0, rel1, rel2, rel3, rel4]);
917+ db.relations.get_relations_for_service(service).map(
918+ function(r) { return r.get('id'); })
919+ .should.eql(['relation-2', 'relation-3', 'relation-4']);
920+ });
921+});
922+
923
924 YUI(GlobalConfig).use(['juju-models', 'juju-gui', 'datasource-local',
925 'juju-tests-utils', 'json-stringify',

Subscribers

People subscribed via source and target branches