Merge lp:~benji/juju-gui/bug-1088507 into lp:juju-gui/experimental
- bug-1088507
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+140020@code.launchpad.net |
Commit message
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/
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_
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.
Gary Poster (gary) wrote : | # |
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:/
File Makefile (right):
https:/
Makefile:293: cp -r --parents */assets
"$(PWD)
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.
- 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
Nicola Larosa (teknico) wrote : | # |
It works now, thanks. Do not worry about the other issue, we'll worry
about it later.
Land this.
Preview Diff
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', |
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 /build- $(1)/juju- ui/assets/ ")
Makefile:293: cp -r --parents */assets
"$(PWD)
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 syspath. join(process. cwd(), charm.js' ));
bin/merge-files:50: paths.push(
'app/models/
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 ui/assets/ node-event- simulate. js"></script>
test/index.html:9: <script
src="/juju-
You explained the horror behind these two inclusions. Please add a
comment so others can enjoy.
https:/ /codereview. appspot. com/6947057/