Merge lp:~benji/juju-gui/megawatcher-propose into lp:juju-gui/experimental

Proposed by Benji York
Status: Merged
Merged at revision: 457
Proposed branch: lp:~benji/juju-gui/megawatcher-propose
Merge into: lp:juju-gui/experimental
Diff against target: 582 lines (+368/-24)
6 files modified
Makefile (+1/-1)
app/store/env/go.js (+148/-4)
app/store/env/python.js (+12/-0)
test/test_env_go.js (+202/-13)
test/utils.js (+5/-5)
undocumented (+0/-1)
To merge this branch: bzr merge lp:~benji/juju-gui/megawatcher-propose
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+155471@code.launchpad.net

Description of the change

Wire up the first bits of the megawatcher.

There is still much left to do, but what is here works and is reasonably
well tested.

https://codereview.appspot.com/7783050/

To post a comment you must log in.
Revision history for this message
Jeff Pihach (hatch) wrote :

LGTM thanks! 'land with changes' I would like to see the two tests
mentioned below switched to use done() and then just take a look at the
one which isn't async but possibly should be?

Thanks again!

https://codereview.appspot.com/7783050/diff/1/app/store/env/go.js
File app/store/env/go.js (right):

https://codereview.appspot.com/7783050/diff/1/app/store/env/go.js#newcode77
app/store/env/go.js:77: instance_id: entityInfo.InstanceId // XXX and
more stuff
Can this be camelCased?

https://codereview.appspot.com/7783050/diff/1/test/test_env_go.js
File test/test_env_go.js (right):

https://codereview.appspot.com/7783050/diff/1/test/test_env_go.js#newcode58
test/test_env_go.js:58: assert.isTrue(loginFired);
Last night I did some reading on the best approach to this and I think
that using the done() method is the best way. This way could potentially
have a race condition causing the test to fail.

see: http://visionmedia.github.com/mocha/#asynchronous-code

https://codereview.appspot.com/7783050/diff/1/test/test_env_go.js#newcode72
test/test_env_go.js:72: assert.isTrue(loginFired);
see done() above

https://codereview.appspot.com/7783050/diff/1/test/test_env_go.js#newcode85
test/test_env_go.js:85: conn.msg({RequestId: 1, Response: {}});
conn.msg() is async above but not here?

https://codereview.appspot.com/7783050/diff/1/test/utils.js
File test/utils.js (right):

https://codereview.appspot.com/7783050/diff/1/test/utils.js#newcode30
test/utils.js:30: if (!back) {
just FYI - another commonly used convention is
back = back || 1;

https://codereview.appspot.com/7783050/

Revision history for this message
Francesco Banconi (frankban) wrote :

This branch looks good, a solid base for the megawatcher stuff.
LGTM with some questions and comments below.
Thank you.

https://codereview.appspot.com/7783050/diff/1/app/store/env/go.js
File app/store/env/go.js (right):

https://codereview.appspot.com/7783050/diff/1/app/store/env/go.js#newcode98
app/store/env/go.js:98: XXX FAKE FAKE FAKE, does not do anything.
Can't we just remove this method?

https://codereview.appspot.com/7783050/diff/1/app/store/env/go.js#newcode148
app/store/env/go.js:148: if (!op.Params) {
Nice, thanks.

https://codereview.appspot.com/7783050/diff/1/app/store/env/go.js#newcode196
app/store/env/go.js:196: deltas.push([kind, operation,
converter(entityInfo)]);
Very nice and simple.

https://codereview.appspot.com/7783050/diff/1/app/store/env/go.js#newcode219
app/store/env/go.js:219: this.fire('_rpc_response', data);
Just curiosity: why an event is preferred here over directly calling
_handleRpcResponse?

https://codereview.appspot.com/7783050/diff/1/test/test_env_go.js
File test/test_env_go.js (right):

https://codereview.appspot.com/7783050/diff/1/test/test_env_go.js#newcode82
test/test_env_go.js:82: it('calls environmentInfo and watchAll ofter
login', function() {
typo: after

https://codereview.appspot.com/7783050/diff/1/test/test_env_go.js#newcode678
test/test_env_go.js:678: assert.isTrue('Id' in msg.Params);
Here we could also test that Params.Id is effectively the answer...

https://codereview.appspot.com/7783050/diff/1/test/test_env_go.js#newcode715
test/test_env_go.js:715: Y = YUI(GlobalConfig).use(['juju-env',
'juju-tests-utils'], function(Y) {
It seems 'juju-tests-utils' is not really required by this test case.
Ditto for all the converter tests below.

https://codereview.appspot.com/7783050/diff/1/undocumented
File undocumented (left):

https://codereview.appspot.com/7783050/diff/1/undocumented#oldcode15
undocumented:15: app/store/env/python.js:371 "get_endpoints"
Thank you.

https://codereview.appspot.com/7783050/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2013-03-22 21:22:38 +0000
3+++ Makefile 2013-03-26 12:15:49 +0000
4@@ -242,7 +242,7 @@
5 app/assets/javascripts/d3.v2.min.js
6
7 gjslint: virtualenv/bin/gjslint
8- virtualenv/bin/gjslint --strict --nojsdoc --jslint_error=all \
9+ virtualenv/bin/gjslint --unix --strict --nojsdoc --jslint_error=all \
10 --custom_jsdoc_tags module,main,class,method,event,property,attribute,submodule,namespace,extends,config,constructor,static,final,readOnly,writeOnce,optional,required,param,return,for,type,private,protected,requires,default,uses,example,chainable,deprecated,since,async,beta,bubbles,extension,extensionfor,extension_for \
11 -x $(LINT_IGNORE) $(JSFILES)
12
13
14=== modified file 'app/store/env/go.js'
15--- app/store/env/go.js 2013-03-22 16:52:05 +0000
16+++ app/store/env/go.js 2013-03-26 12:15:49 +0000
17@@ -29,6 +29,56 @@
18
19 GoEnvironment.NAME = 'go-env';
20
21+ var entityInfoConverters = {
22+ /**
23+ Convert a Go style entity info into the expected legacy format.
24+
25+ @param {Object} entityInfo The JSON entity information from Go.
26+ @return {Object} The legacy JSON data that the GUI expects.
27+ */
28+ service: function(entityInfo) {
29+ return {
30+ id: entityInfo.Name,
31+ exposed: entityInfo.Exposed // XXX and more stuff
32+ };
33+ },
34+ /**
35+ Convert a Go style entity info into the expected legacy format.
36+
37+ @param {Object} entityInfo The JSON entity information from Go.
38+ @return {Object} The legacy JSON data that the GUI expects.
39+ */
40+ unit: function(entityInfo) {
41+ return {
42+ id: entityInfo.Name,
43+ service: entityInfo.Service // XXX and more stuff
44+ };
45+ },
46+ /**
47+ Convert a Go style entity info into the expected legacy format.
48+
49+ @param {Object} entityInfo The JSON entity information from Go.
50+ @return {Object} The legacy JSON data that the GUI expects.
51+ */
52+ relation: function(entityInfo) {
53+ return {
54+ id: entityInfo.Key // XXX and more stuff
55+ };
56+ },
57+ /**
58+ Convert a Go style entity info into the expected legacy format.
59+
60+ @param {Object} entityInfo The JSON entity information from Go.
61+ @return {Object} The legacy JSON data that the GUI expects.
62+ */
63+ machine: function(entityInfo) {
64+ return {
65+ id: entityInfo.Id,
66+ instance_id: entityInfo.InstanceId // XXX and more stuff
67+ };
68+ }
69+ };
70+
71 Y.extend(GoEnvironment, environments.BaseEnvironment, {
72
73 /**
74@@ -41,7 +91,26 @@
75 // Define the default user name for this environment. It will appear as
76 // predefined value in the login mask.
77 this.defaultUser = 'user-admin';
78- },
79+ this.on('_rpc_response', this._handleRpcResponse, this);
80+ },
81+
82+ /**
83+ XXX FAKE FAKE FAKE, does not do anything.
84+
85+ Get the available endpoints (by interface) for a collection of
86+ services.
87+
88+ @method get_endpoints
89+ @param {Array} services Zero or more currently deployed services for
90+ which the endpoints should be collected. Specifying an empty array
91+ indicates that all deployed services should be analyzed.
92+ @param {Function} callback A callable that must be called once the
93+ operation is performed.
94+ @return {undefined} Sends a message to the server only.
95+ */
96+ get_endpoints: function(services, callback) {
97+ },
98+
99
100 /**
101 * See "app.store.env.base.BaseEnvironment.dispatch_result".
102@@ -76,11 +145,83 @@
103 this._txn_callbacks[tid] = callback;
104 }
105 op.RequestId = tid;
106+ if (!op.Params) {
107+ op.Params = {};
108+ }
109 var msg = Y.JSON.stringify(op);
110 this.ws.send(msg);
111 },
112
113 /**
114+ Begin watching all Juju status.
115+
116+ @method _watchAll
117+ @private
118+ @return {undefined} Sends a message to the server only.
119+ */
120+ _watchAll: function() {
121+ this._send_rpc(
122+ {
123+ Type: 'Client',
124+ Request: 'WatchAll'
125+ },
126+ function(data) {
127+ if (data.Error) {
128+ console.log('aiiiiie!'); // retry and eventually alert user XXX
129+ } else {
130+ this._allWatcherId = data.Response.AllWatcherId;
131+ this._next();
132+ }
133+ }
134+ );
135+ },
136+
137+ /**
138+ Process an incoming response to an RPC request.
139+
140+ @method _handleRpcResponse
141+ @param {Object} data The data returned by the server.
142+ @return {undefined} Nothing.
143+ */
144+ _handleRpcResponse: function(data) {
145+ // We do this early to get a response back fast. Might be a bad
146+ // idea. :-)
147+ this._next();
148+ // data.Deltas has our stuff. We need to translate delta
149+ // events based on the deltas we got.
150+ var deltas = [];
151+ data.Response.Deltas.forEach(function(delta) {
152+ var kind = delta[0], operation = delta[1], entityInfo = delta[2];
153+ var converter = entityInfoConverters[kind];
154+ deltas.push([kind, operation, converter(entityInfo)]);
155+ });
156+ this.fire('delta', {data: {result: deltas}});
157+ },
158+
159+ /**
160+ Get the next batch of deltas from the Juju status.
161+
162+ @method _next
163+ @private
164+ @return {undefined} Sends a message to the server only.
165+ */
166+ _next: function() {
167+ this._send_rpc({
168+ Type: 'AllWatcher',
169+ Request: 'Next',
170+ Params: {
171+ Id: this._allWatcherId
172+ }
173+ }, function(data) {
174+ if (data.Error) {
175+ console.log('aiiiiie!'); // XXX
176+ } else {
177+ this.fire('_rpc_response', data);
178+ }
179+ });
180+ },
181+
182+ /**
183 * React to the results of sending a login message to the server.
184 *
185 * @method handleLoginEvent
186@@ -88,10 +229,12 @@
187 * @return {undefined} Nothing.
188 */
189 handleLogin: function(data) {
190+ this.pendingLoginResponse = false;
191 this.userIsAuthenticated = !data.Error;
192 if (this.userIsAuthenticated) {
193 // If login succeeded retrieve the environment info.
194 this.environmentInfo();
195+ this._watchAll();
196 } else {
197 // If the credentials were rejected remove them.
198 this.setCredentials(null);
199@@ -109,7 +252,7 @@
200 */
201 login: function() {
202 // If the user is already authenticated there is nothing to do.
203- if (this.userIsAuthenticated) {
204+ if (this.userIsAuthenticated || this.pendingLoginResponse) {
205 return;
206 }
207 var credentials = this.getCredentials();
208@@ -122,6 +265,7 @@
209 Password: credentials.password
210 }
211 }, this.handleLogin);
212+ this.pendingLoginResponse = true;
213 } else {
214 console.warn('Attempted login without providing credentials.');
215 this.fire('login', {data: {result: false}});
216@@ -156,8 +300,7 @@
217 environmentInfo: function() {
218 this._send_rpc({
219 Type: 'Client',
220- Request: 'EnvironmentInfo',
221- Params: {}
222+ Request: 'EnvironmentInfo'
223 }, this.handleEnvironmentInfo);
224 },
225
226@@ -587,6 +730,7 @@
227 });
228
229 environments.GoEnvironment = GoEnvironment;
230+ environments.entityInfoConverters = entityInfoConverters;
231
232 }, '0.1.0', {
233 requires: [
234
235=== modified file 'app/store/env/python.js'
236--- app/store/env/python.js 2013-02-26 20:04:10 +0000
237+++ app/store/env/python.js 2013-03-26 12:15:49 +0000
238@@ -367,6 +367,18 @@
239 retry: retry || false}, callback, true);
240 },
241
242+ /**
243+ Get the available endpoints (by interface) for a collection of
244+ services.
245+
246+ @method get_endpoints
247+ @param {Array} services Zero or more currently deployed services for
248+ which the endpoints should be collected. Specifying an empty array
249+ indicates that all deployed services should be analyzed.
250+ @param {Function} callback A callable that must be called once the
251+ operation is performed.
252+ @return {undefined} Sends a message to the server only.
253+ */
254 get_endpoints: function(services, callback) {
255 this._send_rpc({'op': 'get_endpoints', 'service_names': services},
256 callback);
257
258=== modified file 'test/test_env_go.js'
259--- test/test_env_go.js 2013-03-22 16:52:05 +0000
260+++ test/test_env_go.js 2013-03-26 12:15:49 +0000
261@@ -13,18 +13,16 @@
262 });
263 });
264
265- beforeEach(function(done) {
266+ beforeEach(function() {
267 conn = new utils.SocketStub();
268 env = juju.newEnvironment({
269 conn: conn, user: 'user', password: 'password'
270 }, 'go');
271 env.connect();
272- done();
273 });
274
275- afterEach(function(done) {
276+ afterEach(function() {
277 env.destroy();
278- done();
279 });
280
281 it('sends the correct login message', function() {
282@@ -47,7 +45,7 @@
283 assert.isTrue(env.failedAuthentication);
284 });
285
286- it('fires a login event on successful login', function(done) {
287+ it('fires a login event on successful login', function() {
288 var loginFired = false;
289 var result;
290 env.on('login', function(evt) {
291@@ -59,10 +57,9 @@
292 conn.msg({RequestId: 1, Response: {}});
293 assert.isTrue(loginFired);
294 assert.isTrue(result);
295- done();
296 });
297
298- it('fires a login event on failed login', function(done) {
299+ it('fires a login event on failed login', function() {
300 var loginFired = false;
301 var result;
302 env.on('login', function(evt) {
303@@ -74,7 +71,6 @@
304 conn.msg({RequestId: 1, Error: 'Invalid user or password'});
305 assert.isTrue(loginFired);
306 assert.isFalse(result);
307- done();
308 });
309
310 it('avoids sending login requests without credentials', function() {
311@@ -83,20 +79,28 @@
312 assert.equal(0, conn.messages.length);
313 });
314
315- it('calls environmentInfo on successful login', function(done) {
316+ it('calls environmentInfo and watchAll ofter login', function() {
317 env.login();
318 // Assume login to be the first request.
319 conn.msg({RequestId: 1, Response: {}});
320- var last_message = conn.last_message();
321+ var environmentInfoMessage = conn.last_message(2);
322 // EnvironmentInfo is the second request.
323- var expected = {
324+ var environmentInfoExpected = {
325 Type: 'Client',
326 Request: 'EnvironmentInfo',
327 RequestId: 2,
328 Params: {}
329 };
330- assert.deepEqual(expected, last_message);
331- done();
332+ assert.deepEqual(environmentInfoExpected, environmentInfoMessage);
333+ var watchAllMessage = conn.last_message();
334+ // EnvironmentInfo is the second request.
335+ var watchAllExpected = {
336+ Type: 'Client',
337+ Request: 'WatchAll',
338+ RequestId: 3,
339+ Params: {}
340+ };
341+ assert.deepEqual(watchAllExpected, watchAllMessage);
342 });
343
344 it('sends the correct request for environment info', function() {
345@@ -651,6 +655,191 @@
346 assert.equal(err, 'service "yoursql" not found');
347 });
348
349+ it('provides for a missing Params', function() {
350+ // If no "Params" are provided in an RPC call an empty one is added.
351+ var op = {};
352+ env._send_rpc(op);
353+ assert.deepEqual(op.Params, {});
354+ });
355+
356+ it('can watch all changes', function() {
357+ env._watchAll();
358+ msg = conn.last_message();
359+ assert.equal(msg.Type, 'Client');
360+ assert.equal(msg.Request, 'WatchAll');
361+ });
362+
363+ it('can retrieve the next set of environment changes', function() {
364+ // This is normally set by _watchAll, we'll fake it here.
365+ env._allWatcherId = 42;
366+ env._next();
367+ msg = conn.last_message();
368+ assert.equal(msg.Type, 'AllWatcher');
369+ assert.isTrue('Id' in msg.Params);
370+ assert.equal(msg.Request, 'Next');
371+ });
372+
373+ it('fires "_rpc_response" message after an RPC response', function(done) {
374+ // We don't want the real response, we just want to be sure the event is
375+ // fired.
376+ env.detach('_rpc_response');
377+ env.on('_rpc_response', function(data) {
378+ done();
379+ });
380+ // Calling this sets up the callback.
381+ env._next();
382+ env._txn_callbacks[env._counter].call(env, {});
383+ // The only test assertion is that done (above) is called.
384+ });
385+
386+ it('fires "delta" when handling an RPC response', function(done) {
387+ env.detach('delta');
388+ var callbackData = {Response: {Deltas: [['service', 'deploy', {}]]}};
389+ env.on('delta', function(data) {
390+ console.log(data.result);
391+ done();
392+ });
393+ env._handleRpcResponse(callbackData);
394+ });
395+
396+ });
397+
398+})();
399+
400+(function() {
401+
402+ describe('Go Juju environment service entity converter', function() {
403+ var environments, utils, Y, converter, entityInfoConverters;
404+
405+ before(function(done) {
406+ Y = YUI(GlobalConfig).use(['juju-env', 'juju-tests-utils'], function(Y) {
407+ environments = Y.namespace('juju.environments');
408+ utils = Y.namespace('juju-tests.utils');
409+ converter = environments.entityInfoConverters.service;
410+ entityInfoConverters = environments.entityInfoConverters;
411+ done();
412+ });
413+ });
414+
415+
416+ it('exists', function() {
417+ assert.isTrue('service' in entityInfoConverters);
418+ });
419+
420+ it('converts "Name" to "id"', function() {
421+ var converted = converter({Name: 'service name'});
422+ assert.isTrue('id' in converted);
423+ assert.equal('service name', converted.id);
424+ });
425+
426+ it('converts "Exposed" to "exposed"', function() {
427+ var converted = converter({Exposed: true});
428+ assert.isTrue('exposed' in converted);
429+ assert.isTrue(converted.exposed);
430+ });
431+
432+ });
433+
434+})();
435+
436+(function() {
437+
438+ describe('Go Juju environment unit entity converter', function() {
439+ var environments, utils, Y, converter, entityInfoConverters;
440+
441+ before(function(done) {
442+ Y = YUI(GlobalConfig).use(['juju-env', 'juju-tests-utils'], function(Y) {
443+ environments = Y.namespace('juju.environments');
444+ utils = Y.namespace('juju-tests.utils');
445+ converter = environments.entityInfoConverters.unit;
446+ entityInfoConverters = environments.entityInfoConverters;
447+ done();
448+ });
449+ });
450+
451+
452+ it('exists', function() {
453+ assert.isTrue('unit' in entityInfoConverters);
454+ });
455+
456+ it('converts "Name" to "id"', function() {
457+ var converted = converter({Name: 'unit name'});
458+ assert.isTrue('id' in converted);
459+ assert.equal('unit name', converted.id);
460+ });
461+
462+ it('converts "Service" to "service"', function() {
463+ var converted = converter({Service: 'a service'});
464+ assert.isTrue('service' in converted);
465+ assert.equal('a service', converted.service);
466+ });
467+
468+ });
469+
470+})();
471+
472+(function() {
473+
474+ describe('Go Juju environment relation entity converter', function() {
475+ var environments, utils, Y, converter, entityInfoConverters;
476+
477+ before(function(done) {
478+ Y = YUI(GlobalConfig).use(['juju-env', 'juju-tests-utils'], function(Y) {
479+ environments = Y.namespace('juju.environments');
480+ utils = Y.namespace('juju-tests.utils');
481+ converter = environments.entityInfoConverters.relation;
482+ entityInfoConverters = environments.entityInfoConverters;
483+ done();
484+ });
485+ });
486+
487+
488+ it('exists', function() {
489+ assert.isTrue('relation' in entityInfoConverters);
490+ });
491+
492+ it('converts "Name" to "id"', function() {
493+ var converted = converter({Key: 'relation name'});
494+ assert.isTrue('id' in converted);
495+ assert.equal('relation name', converted.id);
496+ });
497+
498+ });
499+
500+})();
501+
502+(function() {
503+
504+ describe('Go Juju environment machine entity converter', function() {
505+ var environments, utils, Y, converter, entityInfoConverters;
506+
507+ before(function(done) {
508+ Y = YUI(GlobalConfig).use(['juju-env', 'juju-tests-utils'], function(Y) {
509+ environments = Y.namespace('juju.environments');
510+ utils = Y.namespace('juju-tests.utils');
511+ converter = environments.entityInfoConverters.machine;
512+ entityInfoConverters = environments.entityInfoConverters;
513+ done();
514+ });
515+ });
516+
517+
518+ it('exists', function() {
519+ assert.isTrue('machine' in entityInfoConverters);
520+ });
521+
522+ it('converts "Id" to "id"', function() {
523+ var converted = converter({Id: 'machine ID'});
524+ assert.isTrue('id' in converted);
525+ assert.equal('machine ID', converted.id);
526+ });
527+
528+ it('converts "InstanceId" to "instance_id"', function() {
529+ var converted = converter({InstanceId: 'instance ID'});
530+ assert.isTrue('instance_id' in converted);
531+ assert.equal('instance ID', converted.instance_id);
532+ });
533+
534 });
535
536 })();
537
538=== modified file 'test/utils.js'
539--- test/utils.js 2013-03-21 21:17:03 +0000
540+++ test/utils.js 2013-03-26 12:15:49 +0000
541@@ -10,7 +10,6 @@
542 this.messages = [];
543
544 this.close = function() {
545- //console.log('close stub');
546 this.messages = [];
547 };
548
549@@ -24,16 +23,17 @@
550 };
551
552 this.msg = function(m) {
553- //console.log('serializing env msg', m);
554 this.onmessage({'data': Y.JSON.stringify(m)});
555 };
556
557- this.last_message = function(m) {
558- return this.messages[this.messages.length - 1];
559+ this.last_message = function(back) {
560+ if (!back) {
561+ back = 1;
562+ }
563+ return this.messages[this.messages.length - back];
564 };
565
566 this.send = function(m) {
567- //console.log('socket send', m);
568 this.messages.push(Y.JSON.parse(m));
569 };
570
571
572=== modified file 'undocumented'
573--- undocumented 2013-03-12 11:16:32 +0000
574+++ undocumented 2013-03-26 12:15:49 +0000
575@@ -12,7 +12,6 @@
576 app/store/env/python.js:94 "_dispatch_rpc_result"
577 app/store/env/python.js:195 "get_service"
578 app/store/env/python.js:86 "_dispatch_event"
579-app/store/env/python.js:371 "get_endpoints"
580 app/store/env/python.js:191 "get_charm"
581 app/store/env/base.js:53 "destructor"
582 app/store/env/base.js:38 "initializer"

Subscribers

People subscribed via source and target branches