Merge lp:~frankban/juju-quickstart/interactive-jenvs into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 110
Proposed branch: lp:~frankban/juju-quickstart/interactive-jenvs
Merge into: lp:juju-quickstart
Diff against target: 1162 lines (+528/-104)
8 files modified
quickstart/cli/ui.py (+2/-0)
quickstart/cli/views.py (+115/-19)
quickstart/manage.py (+3/-3)
quickstart/models/jenv.py (+61/-9)
quickstart/tests/cli/test_views.py (+217/-40)
quickstart/tests/helpers.py (+15/-3)
quickstart/tests/models/test_jenv.py (+86/-16)
quickstart/tests/test_manage.py (+29/-14)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/interactive-jenvs
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+244976@code.launchpad.net

Description of the change

Display the jenv environments in interactive mode.

The jenv environments are now displayed in the
interactive session, and it's possible to use them.

Also implemented a simple jenv detail view in uwrid.

Quickstart now also supports highlighting active
environments, i.e. those that are supposed to be
running because they have a corresponding jenv file
in the juju gome.

Tests: `make check`.

QA:
user the new juju, bootstrap one or more environments
with quickstart (use .venv/bin/python juju-quickstart --upload-tools
if you are using a non-local provider).
Start the interactive session, and check those environments are
properly marked as acrive.
Run quickstart again to check it is idempotent.
Create new users and copy the resulting jenv file to ~/.juju/environments/.
Run quickstart interactive session again, and check the
new environments are reported, and it is possible to use them.

https://codereview.appspot.com/188330043/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+244976_code.launchpad.net,

Message:
Please take a look.

Description:
Display the jenv environments in interactive mode.

The jenv environments are now displayed in the
interactive session, and it's possible to use them.

Also implemented a simple jenv detail view in uwrid.

Quickstart now also supports highlighting active
environments, i.e. those that are supposed to be
running because they have a corresponding jenv file
in the juju gome.

Tests: `make check`.

QA:
user the new juju, bootstrap one or more environments
with quickstart (use .venv/bin/python juju-quickstart --upload-tools
if you are using a non-local provider).
Start the interactive session, and check those environments are
properly marked as acrive.
Run quickstart again to check it is idempotent.
Create new users and copy the resulting jenv file to
~/.juju/environments/.
Run quickstart interactive session again, and check the
new environments are reported, and it is possible to use them.

https://code.launchpad.net/~frankban/juju-quickstart/interactive-jenvs/+merge/244976

(do not edit description out of merge proposal)

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

Affected files (+530, -104 lines):
   A [revision details]
   M quickstart/cli/ui.py
   M quickstart/cli/views.py
   M quickstart/manage.py
   M quickstart/models/jenv.py
   M quickstart/tests/cli/test_views.py
   M quickstart/tests/helpers.py
   M quickstart/tests/models/test_jenv.py
   M quickstart/tests/test_manage.py

Revision history for this message
Richard Harding (rharding) wrote :
Revision history for this message
j.c.sackett (jcsackett) wrote :

LGTM, Franceso. Just one question, and it's not directly related to your
branch.

https://codereview.appspot.com/188330043/diff/1/quickstart/tests/helpers.py
File quickstart/tests/helpers.py (right):

https://codereview.appspot.com/188330043/diff/1/quickstart/tests/helpers.py#newcode266
quickstart/tests/helpers.py:266: 'type': '__unknown__',
This isn't from your branch, but why are we listing ec2 as an "unknown"?

https://codereview.appspot.com/188330043/

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

Thanks for the reviews!

https://codereview.appspot.com/188330043/diff/1/quickstart/tests/helpers.py
File quickstart/tests/helpers.py (right):

https://codereview.appspot.com/188330043/diff/1/quickstart/tests/helpers.py#newcode266
quickstart/tests/helpers.py:266: 'type': '__unknown__',
On 2014/12/17 16:13:07, j.c.sackett wrote:
> This isn't from your branch, but why are we listing ec2 as an
"unknown"?

All the jenv files created by "juju user add" don't include the provider
type. So the type can be unknown even if the name suggests a specific
provider.

https://codereview.appspot.com/188330043/

Revision history for this message
j.c.sackett (jcsackett) wrote :

QA notes:

I ran juju-quickstart -i locally.

I saw my ec2 setup with a check, as it is default.
My two local envs were listed, with the active one showing a green
bullet.
My manual provider env was listed with nothing special, as expected.

I simlinked the juju ci jenv into my environments; it was listed as an
imported environment, and I could see its details once selected.

QA OK.

https://codereview.appspot.com/188330043/

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

*** Submitted:

Display the jenv environments in interactive mode.

The jenv environments are now displayed in the
interactive session, and it's possible to use them.

Also implemented a simple jenv detail view in uwrid.

Quickstart now also supports highlighting active
environments, i.e. those that are supposed to be
running because they have a corresponding jenv file
in the juju gome.

Tests: `make check`.

QA:
user the new juju, bootstrap one or more environments
with quickstart (use .venv/bin/python juju-quickstart --upload-tools
if you are using a non-local provider).
Start the interactive session, and check those environments are
properly marked as acrive.
Run quickstart again to check it is idempotent.
Create new users and copy the resulting jenv file to
~/.juju/environments/.
Run quickstart interactive session again, and check the
new environments are reported, and it is possible to use them.

R=rharding, j.c.sackett
CC=
https://codereview.appspot.com/188330043

https://codereview.appspot.com/188330043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'quickstart/cli/ui.py'
--- quickstart/cli/ui.py 2014-01-07 15:41:55 +0000
+++ quickstart/cli/ui.py 2014-12-17 12:34:18 +0000
@@ -31,6 +31,8 @@
31 # See <http://excess.org/urwid/docs/reference/constants.html31 # See <http://excess.org/urwid/docs/reference/constants.html
32 # foreground-and-background-colors>.32 # foreground-and-background-colors>.
33 (None, 'light gray', 'black'),33 (None, 'light gray', 'black'),
34 ('active', 'light green', 'black'),
35 ('active status', 'dark green', 'light gray'),
34 ('dialog', 'dark gray', 'light gray'),36 ('dialog', 'dark gray', 'light gray'),
35 ('dialog header', 'light gray,bold', 'dark blue'),37 ('dialog header', 'light gray,bold', 'dark blue'),
36 ('control alert', 'light red', 'light gray'),38 ('control alert', 'light red', 'light gray'),
3739
=== modified file 'quickstart/cli/views.py'
--- quickstart/cli/views.py 2014-11-10 14:11:11 +0000
+++ quickstart/cli/views.py 2014-12-17 12:34:18 +0000
@@ -115,7 +115,10 @@
115 forms,115 forms,
116 ui,116 ui,
117)117)
118from quickstart.models import envs118from quickstart.models import (
119 envs,
120 jenv,
121)
119122
120123
121def show(view, *args):124def show(view, *args):
@@ -152,7 +155,7 @@
152 raise ui.AppExit((env_db, env_data))155 raise ui.AppExit((env_db, env_data))
153156
154157
155def env_index(app, env_type_db, env_db, save_callable):158def env_index(app, env_type_db, env_db, jenv_db, save_callable):
156 """Show the Juju environments list.159 """Show the Juju environments list.
157160
158 The env_detail view is displayed when the user clicks on an environment.161 The env_detail view is displayed when the user clicks on an environment.
@@ -162,17 +165,22 @@
162 Receives:165 Receives:
163 - env_type_db: the environments meta information;166 - env_type_db: the environments meta information;
164 - env_db: the environments database;167 - env_db: the environments database;
168 - jenv_db: the jenv files database;
165 - save_callable: a function called to save a new environment database.169 - save_callable: a function called to save a new environment database.
166 """170 """
171 # XXX frankban 16/12/2014: this function is too long, subdivide it.
167 env_db = copy.deepcopy(env_db)172 env_db = copy.deepcopy(env_db)
173 jenv_db = copy.deepcopy(jenv_db)
168 # All the environment views return a tuple (new_env_db, env_data).174 # All the environment views return a tuple (new_env_db, env_data).
169 # Set the env_data to None in the case the user quits the application175 # Set the env_data to None in the case the user quits the application
170 # without selecting an environment to use.176 # without selecting an environment to use.
171 app.set_return_value_on_exit((env_db, None))177 app.set_return_value_on_exit((env_db, None))
172 detail_view = functools.partial(178 detail_view = functools.partial(
173 env_detail, app, env_type_db, env_db, save_callable)179 env_detail, app, env_type_db, env_db, jenv_db, save_callable)
180 jenv_view = functools.partial(
181 jenv_detail, app, env_type_db, env_db, jenv_db, save_callable)
174 edit_view = functools.partial(182 edit_view = functools.partial(
175 env_edit, app, env_type_db, env_db, save_callable)183 env_edit, app, env_type_db, env_db, jenv_db, save_callable)
176 # Alphabetically sort the existing environments.184 # Alphabetically sort the existing environments.
177 environments = sorted([185 environments = sorted([
178 envs.get_env_data(env_db, env_name)186 envs.get_env_data(env_db, env_name)
@@ -269,8 +277,9 @@
269 # "juju status" for each environment in the list, which is expensive and277 # "juju status" for each environment in the list, which is expensive and
270 # time consuming.278 # time consuming.
271 focus_position = None279 focus_position = None
272 errors_found = default_found = False280 active_found = default_found = errors_found = False
273 existing_widgets_num = len(widgets)281 existing_widgets_num = len(widgets)
282 remaining_jenv_db = copy.deepcopy(jenv_db)
274 for position, env_data in enumerate(environments):283 for position, env_data in enumerate(environments):
275 bullet = '\N{BULLET}'284 bullet = '\N{BULLET}'
276 # Is this environment the default one?285 # Is this environment the default one?
@@ -279,22 +288,48 @@
279 # The first two positions are the section header and the divider.288 # The first two positions are the section header and the divider.
280 focus_position = position + existing_widgets_num289 focus_position = position + existing_widgets_num
281 bullet = '\N{CHECK MARK}'290 bullet = '\N{CHECK MARK}'
282 # Is this environment valid?291 if remaining_jenv_db['environments'].pop(env_data['name'], None):
283 env_metadata = envs.get_env_metadata(env_type_db, env_data)292 # This is an active environment. Is it running? Who knows...
284 errors = envs.validate(env_metadata, env_data)293 active_found = True
285 if errors:294 bullet = ('active', bullet)
286 errors_found = True295 else:
287 bullet = ('error', bullet)296 # Check if this environment is valid.
297 env_metadata = envs.get_env_metadata(env_type_db, env_data)
298 errors = envs.validate(env_metadata, env_data)
299 if errors:
300 errors_found = True
301 bullet = ('error', bullet)
288 # Create a label for the environment.302 # Create a label for the environment.
289 env_short_description = envs.get_env_short_description(env_data)303 env_short_description = envs.get_env_short_description(env_data)
290 text = [bullet, ' {}'.format(env_short_description)]304 text = [bullet, ' {}'.format(env_short_description)]
291 widgets.append(ui.MenuButton(text, ui.thunk(detail_view, env_data)))305 widgets.append(ui.MenuButton(text, ui.thunk(detail_view, env_data)))
292306
307 # Alphabetically sort the remaining environments not included in the
308 # environments.yaml file.
309 environments = sorted([
310 envs.get_env_data(remaining_jenv_db, env_name)
311 for env_name in remaining_jenv_db['environments']
312 ], key=operator.itemgetter('name'))
313
314 # List the remaining active environments. Those environments are not
315 # included in the environments.yaml file: they are probably imported and
316 # supposed to be working/active. The user has the ability to select them.
317 widgets.extend([
318 urwid.Divider(),
319 urwid.Text(('highlight', 'Other active environments')),
320 urwid.Text('(imported/not included in your environments.yaml file):'),
321 urwid.Divider(),
322 ])
323 bullet = ('active', '\N{BULLET}')
324 for env_data in environments:
325 env_short_description = jenv.get_env_short_description(env_data)
326 text = [bullet, ' {}'.format(env_short_description)]
327 widgets.append(ui.MenuButton(text, ui.thunk(jenv_view, env_data)))
328
293 # Set up the "create a new environment" section.329 # Set up the "create a new environment" section.
294 widgets.extend([330 widgets.extend([
295 urwid.Divider(),331 urwid.Divider(),
296 urwid.Text((332 urwid.Text(('highlight', 'Create a new environment:')),
297 'highlight', 'Create a new environment:')),
298 urwid.Divider(),333 urwid.Divider(),
299 ])334 ])
300 # The Juju GUI can be safely installed in the bootstrap node only if its335 # The Juju GUI can be safely installed in the bootstrap node only if its
@@ -322,6 +357,8 @@
322 status = [' \N{UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW} navigate ']357 status = [' \N{UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW} navigate ']
323 if default_found:358 if default_found:
324 status.append(' \N{CHECK MARK} default ')359 status.append(' \N{CHECK MARK} default ')
360 if active_found:
361 status.extend([('active status', ' \N{BULLET}'), ' active '])
325 if errors_found:362 if errors_found:
326 status.extend([('error status', ' \N{BULLET}'), ' has errors '])363 status.extend([('error status', ' \N{BULLET}'), ' has errors '])
327 app.set_status(status)364 app.set_status(status)
@@ -332,7 +369,7 @@
332 app.set_contents(contents)369 app.set_contents(contents)
333370
334371
335def env_detail(app, env_type_db, env_db, save_callable, env_data):372def env_detail(app, env_type_db, env_db, jenv_db, save_callable, env_data):
336 """Show details on a Juju environment.373 """Show details on a Juju environment.
337374
338 From this view it is possible to start the environment, set it as default,375 From this view it is possible to start the environment, set it as default,
@@ -341,18 +378,20 @@
341 Receives:378 Receives:
342 - env_type_db: the environments meta information;379 - env_type_db: the environments meta information;
343 - env_db: the environments database;380 - env_db: the environments database;
381 - jenv_db: the jenv files database;
344 - save_callable: a function called to save a new environment database;382 - save_callable: a function called to save a new environment database;
345 - env_data: the environment data.383 - env_data: the environment data.
346 """384 """
347 env_db = copy.deepcopy(env_db)385 env_db = copy.deepcopy(env_db)
386 jenv_db = copy.deepcopy(jenv_db)
348 # All the environment views return a tuple (new_env_db, env_data).387 # All the environment views return a tuple (new_env_db, env_data).
349 # Set the env_data to None in the case the user quits the application388 # Set the env_data to None in the case the user quits the application
350 # without selecting an environment to use.389 # without selecting an environment to use.
351 app.set_return_value_on_exit((env_db, None))390 app.set_return_value_on_exit((env_db, None))
352 index_view = functools.partial(391 index_view = functools.partial(
353 env_index, app, env_type_db, env_db, save_callable)392 env_index, app, env_type_db, env_db, jenv_db, save_callable)
354 edit_view = functools.partial(393 edit_view = functools.partial(
355 env_edit, app, env_type_db, env_db, save_callable, env_data)394 env_edit, app, env_type_db, env_db, jenv_db, save_callable, env_data)
356395
357 def use(env_data):396 def use(env_data):
358 # Quit the interactive session returning the (possibly modified)397 # Quit the interactive session returning the (possibly modified)
@@ -425,7 +464,62 @@
425 app.set_contents(listbox)464 app.set_contents(listbox)
426465
427466
428def env_edit(app, env_type_db, env_db, save_callable, env_data):467def jenv_detail(app, env_type_db, env_db, jenv_db, save_callable, env_data):
468 """Show details on a Juju imported environment.
469
470 The environment is not included in the environments.yaml file, but just
471 found in the jenv database.
472 From this view it is possible to start the environment.
473
474 Receives:
475 - env_type_db: the environments meta information;
476 - env_db: the environments database;
477 - jenv_db: the jenv files database;
478 - save_callable: a function called to save a new environment database;
479 - env_data: the environment data.
480 """
481 env_db = copy.deepcopy(env_db)
482 jenv_db = copy.deepcopy(jenv_db)
483 # All the environment views return a tuple (new_env_db, env_data).
484 # Set the env_data to None in the case the user quits the application
485 # without selecting an environment to use.
486 app.set_return_value_on_exit((env_db, None))
487 index_view = functools.partial(
488 env_index, app, env_type_db, env_db, jenv_db, save_callable)
489
490 def use(env_data):
491 # Quit the interactive session returning the (possibly modified)
492 # environment database and the environment data corresponding to the
493 # selected environment.
494 raise ui.AppExit((env_db, env_data))
495
496 app.set_title(jenv.get_env_short_description(env_data))
497 widgets = []
498 for key, value in jenv.get_env_details(env_data):
499 widgets.append(urwid.Text(['{}: '.format(key), ('highlight', value)]))
500 widgets.extend([
501 urwid.Divider(),
502 urwid.Text([
503 ('highlight', 'Imported active environment.\n'),
504 'This environment is not included in your environments.yaml file.'
505 '\nFor this reason, it is not possible to edit or remove it.\n'
506 'However, you can use the link below to ',
507 ('highlight', 'use Juju Quickstart'),
508 ' with this environment.',
509 ]),
510 ])
511
512 controls = [
513 ui.MenuButton('back', ui.thunk(index_view)),
514 ui.MenuButton('use', ui.thunk(use, env_data)),
515 ]
516 widgets.append(ui.create_controls(*controls))
517 listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
518 app.set_contents(listbox)
519 app.set_status([' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate '])
520
521
522def env_edit(app, env_type_db, env_db, jenv_db, save_callable, env_data):
429 """Create or modify a Juju environment.523 """Create or modify a Juju environment.
430524
431 This view displays an edit form allowing for environment525 This view displays an edit form allowing for environment
@@ -435,6 +529,7 @@
435 Receives:529 Receives:
436 - env_type_db: the environments meta information;530 - env_type_db: the environments meta information;
437 - env_db: the environments database;531 - env_db: the environments database;
532 - jenv_db: the jenv files database;
438 - save_callable: a function called to save a new environment database;533 - save_callable: a function called to save a new environment database;
439 - env_data: the environment data.534 - env_data: the environment data.
440535
@@ -444,15 +539,16 @@
444 env_data includes the "name" key and all the other environment info.539 env_data includes the "name" key and all the other environment info.
445 """540 """
446 env_db = copy.deepcopy(env_db)541 env_db = copy.deepcopy(env_db)
542 jenv_db = copy.deepcopy(jenv_db)
447 # All the environment views return a tuple (new_env_db, env_data).543 # All the environment views return a tuple (new_env_db, env_data).
448 # Set the env_data to None in the case the user quits the application544 # Set the env_data to None in the case the user quits the application
449 # without selecting an environment to use.545 # without selecting an environment to use.
450 app.set_return_value_on_exit((env_db, None))546 app.set_return_value_on_exit((env_db, None))
451 env_metadata = envs.get_env_metadata(env_type_db, env_data)547 env_metadata = envs.get_env_metadata(env_type_db, env_data)
452 index_view = functools.partial(548 index_view = functools.partial(
453 env_index, app, env_type_db, env_db, save_callable)549 env_index, app, env_type_db, env_db, jenv_db, save_callable)
454 detail_view = functools.partial(550 detail_view = functools.partial(
455 env_detail, app, env_type_db, env_db, save_callable)551 env_detail, app, env_type_db, env_db, jenv_db, save_callable)
456 if 'name' in env_data:552 if 'name' in env_data:
457 exists = True553 exists = True
458 title = 'Edit the {} environment'554 title = 'Edit the {} environment'
459555
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2014-12-16 14:12:44 +0000
+++ quickstart/manage.py 2014-12-17 12:34:18 +0000
@@ -203,7 +203,7 @@
203 return save_callable203 return save_callable
204204
205205
206def _start_interactive_session(parser, env_type_db, env_db, env_file):206def _start_interactive_session(parser, env_type_db, env_db, jenv_db, env_file):
207 """Start the Urwid interactive session.207 """Start the Urwid interactive session.
208208
209 Return the env_data corresponding to the user selected environment.209 Return the env_data corresponding to the user selected environment.
@@ -212,7 +212,7 @@
212 """212 """
213 save_callable = _create_save_callable(parser, env_file)213 save_callable = _create_save_callable(parser, env_file)
214 new_env_db, env_data = views.show(214 new_env_db, env_data = views.show(
215 views.env_index, env_type_db, env_db, save_callable)215 views.env_index, env_type_db, env_db, jenv_db, save_callable)
216 if new_env_db != env_db:216 if new_env_db != env_db:
217 print('changes to the environments file have been saved')217 print('changes to the environments file have been saved')
218 if env_data is None:218 if env_data is None:
@@ -290,7 +290,7 @@
290 if interactive:290 if interactive:
291 # Start the interactive session.291 # Start the interactive session.
292 env_data = _start_interactive_session(292 env_data = _start_interactive_session(
293 parser, env_type_db, env_db, env_file)293 parser, env_type_db, env_db, jenv_db, env_file)
294 else:294 else:
295 # This is a non-interactive session and we need to validate the295 # This is a non-interactive session and we need to validate the
296 # selected environment before proceeding.296 # selected environment before proceeding.
297297
=== modified file 'quickstart/models/jenv.py'
--- quickstart/models/jenv.py 2014-12-16 15:53:25 +0000
+++ quickstart/models/jenv.py 2014-12-17 12:34:18 +0000
@@ -130,9 +130,11 @@
130 When a jenv file is created using "juju user add", the resulting YAML data130 When a jenv file is created using "juju user add", the resulting YAML data
131 is very concise, not even including the environment type.131 is very concise, not even including the environment type.
132 For this reason, the environment database returned by this function does132 For this reason, the environment database returned by this function does
133 not contain the usual fields used as bootstrap options, but just the133 not contain the usual fields used as bootstrap options.
134 environment name and the type. If the environment type is not included in134 The included fields are: "name", "type", "user" and "state-servers".
135 the jenv, UNKNOWN_ENV_TYPE is used.135
136 If the environment type is not included in the jenv, UNKNOWN_ENV_TYPE is
137 used.
136 """138 """
137 db = {'environments': {}}139 db = {'environments': {}}
138 path = os.path.expanduser(os.path.join(settings.JUJU_HOME, 'environments'))140 path = os.path.expanduser(os.path.join(settings.JUJU_HOME, 'environments'))
@@ -153,7 +155,7 @@
153 # Validate the jenv contents.155 # Validate the jenv contents.
154 try:156 try:
155 data = serializers.yaml_load_from_path(fullpath)157 data = serializers.yaml_load_from_path(fullpath)
156 validate(data)158 credentials, servers = validate(data)
157 except ValueError as err:159 except ValueError as err:
158 logging.warn('ignoring invalid jenv file {}: {}'. format(160 logging.warn('ignoring invalid jenv file {}: {}'. format(
159 filename, bytes(err).decode('utf-8')))161 filename, bytes(err).decode('utf-8')))
@@ -164,7 +166,11 @@
164 except ValueError:166 except ValueError:
165 # This is expected when a jenv is generated with "juju user add".167 # This is expected when a jenv is generated with "juju user add".
166 env_type = UNKNOWN_ENV_TYPE168 env_type = UNKNOWN_ENV_TYPE
167 environments[name] = {'type': env_type}169 environments[name] = {
170 'type': env_type,
171 'user': credentials[0],
172 'state-servers': servers,
173 }
168 return db174 return db
169175
170176
@@ -178,13 +184,59 @@
178 - the environment file is not found;184 - the environment file is not found;
179 - the environment file contents are not parsable by YAML;185 - the environment file contents are not parsable by YAML;
180 - the environment data is not valid.186 - the environment data is not valid.
181 """187
182 _get_credentials(data)188 Return the environment credentials and state servers.
189 """
190 credentials = _get_credentials(data)
191 servers = _get_state_servers(data)
192 if not len(servers):
193 raise ValueError(b'no state-servers found')
194 return credentials, servers
195
196
197def get_env_short_description(env_data):
198 """Return a short description of the given env_data.
199
200 The given env_data must include at least the "name" and "type" keys.
201 """
202 parts = [env_data['name']]
203 env_type = env_data['type']
204 if env_type != UNKNOWN_ENV_TYPE:
205 parts.append('(type: {})'.format(env_type))
206 return ' '.join(parts)
207
208
209def get_env_details(env_data):
210 """Return the environment details as a sequence of tuples (label, value).
211
212 In each tuple, the label is the field name, and the value is the
213 corresponding value in the given env_data.
214 """
215 details = []
216 # Add the environment type.
217 env_type = env_data['type']
218 if env_type != UNKNOWN_ENV_TYPE:
219 details.append(('type', env_type))
220 # Add the environment name and user.
221 details.extend([
222 ('name', env_data['name']),
223 ('user', env_data['user']),
224 ])
225 # Add the state servers.
226 servers = ', '.join(env_data['state-servers'])
227 details.append(('state servers', servers))
228 return details
229
230
231def _get_state_servers(data):
232 """Return a tuple of Juju state servers for the given jenv data.
233
234 Raise a ValueError if the state servers cannot be retrieved.
235 """
183 servers = data.get('state-servers')236 servers = data.get('state-servers')
184 if not isinstance(servers, (list, tuple)):237 if not isinstance(servers, (list, tuple)):
185 raise ValueError(b'invalid state-servers field')238 raise ValueError(b'invalid state-servers field')
186 if not len(servers):239 return tuple(servers)
187 raise ValueError(b'no state-servers found')
188240
189241
190def _get_value_from_yaml(data, *args):242def _get_value_from_yaml(data, *args):
191243
=== modified file 'quickstart/tests/cli/test_views.py'
--- quickstart/tests/cli/test_views.py 2014-11-12 15:41:40 +0000
+++ quickstart/tests/cli/test_views.py 2014-12-17 12:34:18 +0000
@@ -34,7 +34,10 @@
34 ui,34 ui,
35 views,35 views,
36)36)
37from quickstart.models import envs37from quickstart.models import (
38 envs,
39 jenv,
40)
38from quickstart.tests import helpers41from quickstart.tests import helpers
39from quickstart.tests.cli import helpers as cli_helpers42from quickstart.tests.cli import helpers as cli_helpers
4043
@@ -178,7 +181,9 @@
178 # a tuple including a copy of the given env_db and None, the latter181 # a tuple including a copy of the given env_db and None, the latter
179 # meaning no environment has been selected.182 # meaning no environment has been selected.
180 env_db = helpers.make_env_db()183 env_db = helpers.make_env_db()
181 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)184 jenv_db = helpers.make_jenv_db()
185 views.env_index(
186 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
182 new_env_db, env_data = self.get_on_exit_return_value(self.loop)187 new_env_db, env_data = self.get_on_exit_return_value(self.loop)
183 self.assertEqual(env_db, new_env_db)188 self.assertEqual(env_db, new_env_db)
184 self.assertIsNot(env_db, new_env_db)189 self.assertIsNot(env_db, new_env_db)
@@ -187,7 +192,9 @@
187 def test_view_title(self):192 def test_view_title(self):
188 # The application title is correctly set up.193 # The application title is correctly set up.
189 env_db = helpers.make_env_db()194 env_db = helpers.make_env_db()
190 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)195 jenv_db = helpers.make_jenv_db()
196 views.env_index(
197 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
191 self.assertEqual(198 self.assertEqual(
192 'Select an existing Juju environment or create a new one',199 'Select an existing Juju environment or create a new one',
193 self.app.get_title())200 self.app.get_title())
@@ -195,7 +202,9 @@
195 def test_view_title_no_environments(self):202 def test_view_title_no_environments(self):
196 # The application title changes if the env_db has no environments.203 # The application title changes if the env_db has no environments.
197 env_db = {'environments': {}}204 env_db = {'environments': {}}
198 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)205 jenv_db = helpers.make_jenv_db()
206 views.env_index(
207 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
199 self.assertEqual(208 self.assertEqual(
200 'No Juju environments already set up: please create one',209 'No Juju environments already set up: please create one',
201 self.app.get_title())210 self.app.get_title())
@@ -204,9 +213,11 @@
204 # The view displays a list of the environments in env_db, and buttons213 # The view displays a list of the environments in env_db, and buttons
205 # to create new environments.214 # to create new environments.
206 env_db = helpers.make_env_db()215 env_db = helpers.make_env_db()
216 jenv_db = {'environments': {}}
207 with local_envs_supported(True):217 with local_envs_supported(True):
208 views.env_index(218 views.env_index(
209 self.app, self.env_type_db, env_db, self.save_callable)219 self.app, self.env_type_db, env_db, jenv_db,
220 self.save_callable)
210 buttons = self.get_widgets_in_contents(221 buttons = self.get_widgets_in_contents(
211 filter_function=self.is_a(ui.MenuButton))222 filter_function=self.is_a(ui.MenuButton))
212 # A button is created for each existing environment (see details) and223 # A button is created for each existing environment (see details) and
@@ -215,13 +226,39 @@
215 expected_buttons_number = len(env_db['environments']) + len(env_types)226 expected_buttons_number = len(env_db['environments']) + len(env_types)
216 self.assertEqual(expected_buttons_number, len(buttons))227 self.assertEqual(expected_buttons_number, len(buttons))
217228
229 def test_view_contents_with_imported_envs(self):
230 # The view displays a list of active imported environments, and buttons
231 # to create new environments.
232 env_db = {'environments': {}}
233 jenv_db = helpers.make_jenv_db()
234 with local_envs_supported(True):
235 views.env_index(
236 self.app, self.env_type_db, env_db, jenv_db,
237 self.save_callable)
238 buttons = self.get_widgets_in_contents(
239 filter_function=self.is_a(ui.MenuButton))
240 # A button is created for each existing environment (see details) and
241 # for each environment type supported by quickstart (create).
242 env_types = envs.get_supported_env_types(self.env_type_db)
243 expected_buttons_number = (
244 # The number of active environments.
245 len(jenv_db['environments']) +
246 # The buttons to create new environments.
247 len(env_types) +
248 # The button to automatically create a new local environment.
249 1
250 )
251 self.assertEqual(expected_buttons_number, len(buttons))
252
218 def test_new_local_environment_disabled(self):253 def test_new_local_environment_disabled(self):
219 # The option to create a new local environment is not present if they254 # The option to create a new local environment is not present if they
220 # are not supported in the current platform.255 # are not supported in the current platform.
221 env_db = helpers.make_env_db()256 env_db = helpers.make_env_db()
257 jenv_db = helpers.make_jenv_db()
222 with local_envs_supported(False):258 with local_envs_supported(False):
223 views.env_index(259 views.env_index(
224 self.app, self.env_type_db, env_db, self.save_callable)260 self.app, self.env_type_db, env_db, jenv_db,
261 self.save_callable)
225 buttons = self.get_widgets_in_contents(262 buttons = self.get_widgets_in_contents(
226 filter_function=self.is_a(ui.MenuButton))263 filter_function=self.is_a(ui.MenuButton))
227 captions = map(cli_helpers.get_button_caption, buttons)264 captions = map(cli_helpers.get_button_caption, buttons)
@@ -235,7 +272,9 @@
235 def test_environment_clicked(self, mock_env_detail):272 def test_environment_clicked(self, mock_env_detail):
236 # The environment detail view is called when clicking an environment.273 # The environment detail view is called when clicking an environment.
237 env_db = helpers.make_env_db()274 env_db = helpers.make_env_db()
238 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)275 jenv_db = {'environments': {}}
276 views.env_index(
277 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
239 buttons = self.get_widgets_in_contents(278 buttons = self.get_widgets_in_contents(
240 filter_function=self.is_a(ui.MenuButton))279 filter_function=self.is_a(ui.MenuButton))
241 # The environments are listed in alphabetical order.280 # The environments are listed in alphabetical order.
@@ -250,20 +289,51 @@
250 # corresponding environment data.289 # corresponding environment data.
251 cli_helpers.emit(button)290 cli_helpers.emit(button)
252 mock_env_detail.assert_called_once_with(291 mock_env_detail.assert_called_once_with(
253 self.app, self.env_type_db, env_db, self.save_callable,292 self.app, self.env_type_db, env_db, jenv_db,
254 env_data)293 self.save_callable, env_data)
255 # Reset the mock so that it does not include any calls on the next294 # Reset the mock so that it does not include any calls on the next
256 # loop cycle.295 # loop cycle.
257 mock_env_detail.reset_mock()296 mock_env_detail.reset_mock()
258297
298 @mock.patch('quickstart.cli.views.jenv_detail')
299 def test_imported_environment_clicked(self, mock_jenv_detail):
300 # The jenv detail view is called when clicking an imported environment.
301 env_db = {'environments': {}}
302 jenv_db = helpers.make_jenv_db()
303 with local_envs_supported(False):
304 views.env_index(
305 self.app, self.env_type_db, env_db, jenv_db,
306 self.save_callable)
307 buttons = self.get_widgets_in_contents(
308 filter_function=self.is_a(ui.MenuButton))
309 # The environments are listed in alphabetical order.
310 environments = sorted(jenv_db['environments'])
311 for env_name, button in zip(environments, buttons):
312 env_data = envs.get_env_data(jenv_db, env_name)
313 # The caption includes the environment description.
314 env_description = jenv.get_env_short_description(env_data)
315 self.assertIn(
316 env_description, cli_helpers.get_button_caption(button))
317 # When the button is clicked, the jenv detail view is called
318 # passing the corresponding environment data.
319 cli_helpers.emit(button)
320 mock_jenv_detail.assert_called_once_with(
321 self.app, self.env_type_db, env_db, jenv_db,
322 self.save_callable, env_data)
323 # Reset the mock so that it does not include any calls on the next
324 # loop cycle.
325 mock_jenv_detail.reset_mock()
326
259 @mock.patch('quickstart.cli.views.env_edit')327 @mock.patch('quickstart.cli.views.env_edit')
260 def test_create_new_environment_clicked(self, mock_env_edit):328 def test_create_new_environment_clicked(self, mock_env_edit):
261 # The environment edit view is called when clicking to create a new329 # The environment edit view is called when clicking to create a new
262 # environment.330 # environment.
263 env_db = helpers.make_env_db()331 env_db = helpers.make_env_db()
332 jenv_db = {'environments': {}}
264 with local_envs_supported(True):333 with local_envs_supported(True):
265 views.env_index(334 views.env_index(
266 self.app, self.env_type_db, env_db, self.save_callable)335 self.app, self.env_type_db, env_db, jenv_db,
336 self.save_callable)
267 buttons = self.get_widgets_in_contents(337 buttons = self.get_widgets_in_contents(
268 filter_function=self.is_a(ui.MenuButton))338 filter_function=self.is_a(ui.MenuButton))
269 env_types = envs.get_supported_env_types(self.env_type_db)339 env_types = envs.get_supported_env_types(self.env_type_db)
@@ -277,9 +347,11 @@
277 # corresponding environment data.347 # corresponding environment data.
278 cli_helpers.emit(button)348 cli_helpers.emit(button)
279 mock_env_edit.assert_called_once_with(349 mock_env_edit.assert_called_once_with(
280 self.app, self.env_type_db, env_db, self.save_callable,350 self.app, self.env_type_db, env_db, jenv_db,
281 {'type': env_type,351 self.save_callable, {
282 'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1]})352 'type': env_type,
353 'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1],
354 })
283 # Reset the mock so that it does not include any calls on the next355 # Reset the mock so that it does not include any calls on the next
284 # loop cycle.356 # loop cycle.
285 mock_env_edit.reset_mock()357 mock_env_edit.reset_mock()
@@ -290,10 +362,12 @@
290 # If that option is clicked, the view quits the application returning362 # If that option is clicked, the view quits the application returning
291 # the newly created env_data.363 # the newly created env_data.
292 env_db = envs.create_empty_env_db()364 env_db = envs.create_empty_env_db()
365 jenv_db = helpers.make_jenv_db()
293 with maas_env_detected(False):366 with maas_env_detected(False):
294 with local_envs_supported(True):367 with local_envs_supported(True):
295 views.env_index(368 views.env_index(
296 self.app, self.env_type_db, env_db, self.save_callable)369 self.app, self.env_type_db, env_db, jenv_db,
370 self.save_callable)
297 buttons = self.get_widgets_in_contents(371 buttons = self.get_widgets_in_contents(
298 filter_function=self.is_a(ui.MenuButton))372 filter_function=self.is_a(ui.MenuButton))
299 # The "create and bootstrap" button is the first one in the contents.373 # The "create and bootstrap" button is the first one in the contents.
@@ -314,9 +388,11 @@
314 # environment is not displayed if the current platform does not support388 # environment is not displayed if the current platform does not support
315 # local environments.389 # local environments.
316 env_db = envs.create_empty_env_db()390 env_db = envs.create_empty_env_db()
391 jenv_db = helpers.make_jenv_db()
317 with local_envs_supported(False):392 with local_envs_supported(False):
318 views.env_index(393 views.env_index(
319 self.app, self.env_type_db, env_db, self.save_callable)394 self.app, self.env_type_db, env_db, jenv_db,
395 self.save_callable)
320 buttons = self.get_widgets_in_contents(396 buttons = self.get_widgets_in_contents(
321 filter_function=self.is_a(ui.MenuButton))397 filter_function=self.is_a(ui.MenuButton))
322 # No "create and bootstrap local" buttons are present.398 # No "create and bootstrap local" buttons are present.
@@ -330,9 +406,11 @@
330 # If that option is clicked, the view quits the application returning406 # If that option is clicked, the view quits the application returning
331 # the newly created env_data.407 # the newly created env_data.
332 env_db = envs.create_empty_env_db()408 env_db = envs.create_empty_env_db()
409 jenv_db = helpers.make_jenv_db()
333 with maas_env_detected(True):410 with maas_env_detected(True):
334 views.env_index(411 views.env_index(
335 self.app, self.env_type_db, env_db, self.save_callable)412 self.app, self.env_type_db, env_db, jenv_db,
413 self.save_callable)
336 buttons = self.get_widgets_in_contents(414 buttons = self.get_widgets_in_contents(
337 filter_function=self.is_a(ui.MenuButton))415 filter_function=self.is_a(ui.MenuButton))
338 # The "create and bootstrap" button is the first one in the contents.416 # The "create and bootstrap" button is the first one in the contents.
@@ -353,9 +431,11 @@
353 # environment is not displayed if no MAAS API endpoints are431 # environment is not displayed if no MAAS API endpoints are
354 # available on the system432 # available on the system
355 env_db = envs.create_empty_env_db()433 env_db = envs.create_empty_env_db()
434 jenv_db = helpers.make_jenv_db()
356 with maas_env_detected(False):435 with maas_env_detected(False):
357 views.env_index(436 views.env_index(
358 self.app, self.env_type_db, env_db, self.save_callable)437 self.app, self.env_type_db, env_db, jenv_db,
438 self.save_callable)
359 buttons = self.get_widgets_in_contents(439 buttons = self.get_widgets_in_contents(
360 filter_function=self.is_a(ui.MenuButton))440 filter_function=self.is_a(ui.MenuButton))
361 # No "create and bootstrap MAAS" buttons are present.441 # No "create and bootstrap MAAS" buttons are present.
@@ -365,7 +445,9 @@
365 def test_selected_environment(self):445 def test_selected_environment(self):
366 # The default environment is already selected in the list.446 # The default environment is already selected in the list.
367 env_db = helpers.make_env_db(default='lxc')447 env_db = helpers.make_env_db(default='lxc')
368 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)448 jenv_db = helpers.make_jenv_db()
449 views.env_index(
450 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
369 env_data = envs.get_env_data(env_db, 'lxc')451 env_data = envs.get_env_data(env_db, 'lxc')
370 env_description = envs.get_env_short_description(env_data)452 env_description = envs.get_env_short_description(env_data)
371 contents = self.app.get_contents()453 contents = self.app.get_contents()
@@ -377,31 +459,50 @@
377 def test_status_with_errors(self):459 def test_status_with_errors(self):
378 # The status message explains how errors are displayed.460 # The status message explains how errors are displayed.
379 env_db = helpers.make_env_db()461 env_db = helpers.make_env_db()
380 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)462 jenv_db = {'environments': {}}
463 views.env_index(
464 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
381 status = self.app.get_status()465 status = self.app.get_status()
382 self.assertEqual(self.base_status + ' \N{BULLET} has errors ', status)466 self.assertEqual(self.base_status + ' \N{BULLET} has errors ', status)
383467
384 def test_status_with_default(self):468 def test_status_with_default(self):
385 # The status message explains how default environment is represented.469 # The status message explains how default environment is represented.
386 env_db = helpers.make_env_db(default='lxc', exclude_invalid=True)470 env_db = helpers.make_env_db(default='lxc', exclude_invalid=True)
387 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)471 jenv_db = {'environments': {}}
472 views.env_index(
473 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
388 status = self.app.get_status()474 status = self.app.get_status()
389 self.assertEqual(self.base_status + ' \N{CHECK MARK} default ', status)475 self.assertEqual(self.base_status + ' \N{CHECK MARK} default ', status)
390476
391 def test_status_with_default_and_errors(self):477 def test_status_with_active(self):
392 # The status message includes both default and errors explanations.478 # The status message explains how active environments are displayed.
479 env_db = helpers.make_env_db(exclude_invalid=True)
480 jenv_db = helpers.make_jenv_db()
481 views.env_index(
482 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
483 status = self.app.get_status()
484 self.assertEqual(self.base_status + ' \N{BULLET} active ', status)
485
486 def test_complete_status(self):
487 # The status message includes default, active and errors explanations.
393 env_db = helpers.make_env_db(default='lxc')488 env_db = helpers.make_env_db(default='lxc')
394 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)489 jenv_db = helpers.make_jenv_db()
490 views.env_index(
491 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
395 status = self.app.get_status()492 status = self.app.get_status()
396 self.assertEqual(493 self.assertEqual(
397 self.base_status +494 self.base_status +
398 ' \N{CHECK MARK} default \N{BULLET} has errors ',495 ' \N{CHECK MARK} default ' +
496 ' \N{BULLET} active ' +
497 ' \N{BULLET} has errors ',
399 status)498 status)
400499
401 def test_status(self):500 def test_base_status(self):
402 # The status only includes navigation info if there are no errors.501 # The status only includes navigation info if there are no errors.
403 env_db = helpers.make_env_db(exclude_invalid=True)502 env_db = helpers.make_env_db(exclude_invalid=True)
404 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)503 jenv_db = {'environments': {}}
504 views.env_index(
505 self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
405 status = self.app.get_status()506 status = self.app.get_status()
406 self.assertEqual(self.base_status, status)507 self.assertEqual(self.base_status, status)
407508
@@ -410,13 +511,14 @@
410511
411 base_status = ' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate '512 base_status = ' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate '
412 env_db = helpers.make_env_db(default='lxc')513 env_db = helpers.make_env_db(default='lxc')
514 jenv_db = helpers.make_jenv_db()
413515
414 def call_view(self, env_name='lxc'):516 def call_view(self, env_name='lxc'):
415 """Call the view passing the env_data corresponding to env_name."""517 """Call the view passing the env_data corresponding to env_name."""
416 self.env_data = envs.get_env_data(self.env_db, env_name)518 self.env_data = envs.get_env_data(self.env_db, env_name)
417 return views.env_detail(519 return views.env_detail(
418 self.app, self.env_type_db, self.env_db, self.save_callable,520 self.app, self.env_type_db, self.env_db, self.jenv_db,
419 self.env_data)521 self.save_callable, self.env_data)
420522
421 def test_view_default_return_value_on_exit(self):523 def test_view_default_return_value_on_exit(self):
422 # The view configures the app so that the return value on user exit is524 # The view configures the app so that the return value on user exit is
@@ -483,7 +585,8 @@
483 back_button = self.get_control_buttons()[0]585 back_button = self.get_control_buttons()[0]
484 cli_helpers.emit(back_button)586 cli_helpers.emit(back_button)
485 mock_env_index.assert_called_once_with(587 mock_env_index.assert_called_once_with(
486 self.app, self.env_type_db, self.env_db, self.save_callable)588 self.app, self.env_type_db, self.env_db, self.jenv_db,
589 self.save_callable)
487590
488 def test_use_button(self):591 def test_use_button(self):
489 # The application exits if the "use" button is clicked.592 # The application exits if the "use" button is clicked.
@@ -522,8 +625,8 @@
522 edit_button = self.get_control_buttons()[3]625 edit_button = self.get_control_buttons()[3]
523 cli_helpers.emit(edit_button)626 cli_helpers.emit(edit_button)
524 mock_env_edit.assert_called_once_with(627 mock_env_edit.assert_called_once_with(
525 self.app, self.env_type_db, self.env_db, self.save_callable,628 self.app, self.env_type_db, self.env_db, self.jenv_db,
526 self.env_data)629 self.save_callable, self.env_data)
527630
528 def test_remove_button(self):631 def test_remove_button(self):
529 # A confirmation dialog is displayed if the "remove" button is clicked.632 # A confirmation dialog is displayed if the "remove" button is clicked.
@@ -598,9 +701,82 @@
598 self.assertEqual(self.base_status, status)701 self.assertEqual(self.base_status, status)
599702
600703
704class TestJenvDetail(EnvViewTestsMixin, unittest.TestCase):
705
706 env_db = helpers.make_env_db(default='lxc')
707 jenv_db = helpers.make_jenv_db()
708
709 def call_view(self, env_name='lxc'):
710 """Call the view passing the env_data corresponding to env_name."""
711 self.env_data = envs.get_env_data(self.jenv_db, env_name)
712 return views.jenv_detail(
713 self.app, self.env_type_db, self.env_db, self.jenv_db,
714 self.save_callable, self.env_data)
715
716 def test_view_default_return_value_on_exit(self):
717 # The view configures the app so that the return value on user exit is
718 # a tuple including a copy of the given env_db and None, the latter
719 # meaning no environment has been selected (for now).
720 self.call_view()
721 new_env_db, env_data = self.get_on_exit_return_value(self.loop)
722 self.assertEqual(self.env_db, new_env_db)
723 self.assertIsNot(self.env_db, new_env_db)
724 self.assertIsNone(env_data)
725
726 def test_view_title(self):
727 # The application title is correctly set up: it shows the description
728 # of the current jenv environment.
729 self.call_view()
730 env_description = jenv.get_env_short_description(self.env_data)
731 self.assertEqual(env_description, self.app.get_title())
732
733 def test_view_contents(self):
734 # The view displays the jenv details.
735 self.call_view()
736 widgets = self.get_widgets_in_contents(
737 filter_function=self.is_a(urwid.Text))
738 expected_texts = [
739 '{}: {}'.format(label, value) for label, value
740 in jenv.get_env_details(self.env_data)
741 ]
742 for expected_text, widget in zip(expected_texts, widgets):
743 self.assertEqual(expected_text, widget.text)
744
745 def test_view_buttons(self):
746 # The "back" and "use" buttons are displayed.
747 self.call_view(env_name='ec2-west')
748 buttons = self.get_control_buttons()
749 captions = map(cli_helpers.get_button_caption, buttons)
750 self.assertEqual(['back', 'use'], captions)
751
752 @mock.patch('quickstart.cli.views.env_index')
753 def test_back_button(self, mock_env_index):
754 # The index view is called if the "back" button is clicked.
755 self.call_view(env_name='ec2-west')
756 # The "back" button is the first one.
757 back_button = self.get_control_buttons()[0]
758 cli_helpers.emit(back_button)
759 mock_env_index.assert_called_once_with(
760 self.app, self.env_type_db, self.env_db, self.jenv_db,
761 self.save_callable)
762
763 def test_use_button(self):
764 # The application exits if the "use" button is clicked.
765 # The env_db and the current environment data are returned.
766 self.call_view(env_name='ec2-west')
767 # The "use" button is the second one.
768 use_button = self.get_control_buttons()[1]
769 with self.assertRaises(ui.AppExit) as context_manager:
770 cli_helpers.emit(use_button)
771 expected_return_value = (self.env_db, self.env_data)
772 self.assertEqual(
773 expected_return_value, context_manager.exception.return_value)
774
775
601class TestEnvEdit(EnvViewTestsMixin, unittest.TestCase):776class TestEnvEdit(EnvViewTestsMixin, unittest.TestCase):
602777
603 env_db = helpers.make_env_db(default='lxc')778 env_db = helpers.make_env_db(default='lxc')
779 jenv_db = helpers.make_jenv_db()
604780
605 def call_view(self, env_name='lxc', env_type=None):781 def call_view(self, env_name='lxc', env_type=None):
606 """Call the view passing the env_data corresponding to env_name.782 """Call the view passing the env_data corresponding to env_name.
@@ -613,8 +789,8 @@
613 else:789 else:
614 self.env_data = {'type': env_type}790 self.env_data = {'type': env_type}
615 return views.env_edit(791 return views.env_edit(
616 self.app, self.env_type_db, self.env_db, self.save_callable,792 self.app, self.env_type_db, self.env_db, self.jenv_db,
617 self.env_data)793 self.save_callable, self.env_data)
618794
619 def get_form_contents(self):795 def get_form_contents(self):
620 """Return the form contents included in the app page.796 """Return the form contents included in the app page.
@@ -749,8 +925,8 @@
749 'ec2-west successfully modified', self.app.get_message())925 'ec2-west successfully modified', self.app.get_message())
750 # The application displays the environment detail view.926 # The application displays the environment detail view.
751 mock_env_detail.assert_called_once_with(927 mock_env_detail.assert_called_once_with(
752 self.app, self.env_type_db, self.env_db, self.save_callable,928 self.app, self.env_type_db, self.env_db, self.jenv_db,
753 new_env_data)929 self.save_callable, new_env_data)
754930
755 @mock.patch('quickstart.cli.views.env_detail')931 @mock.patch('quickstart.cli.views.env_detail')
756 def test_save_empty_db(self, mock_env_detail):932 def test_save_empty_db(self, mock_env_detail):
@@ -769,8 +945,8 @@
769 expected_new_env_data.update({'type': 'local', 'is-default': True})945 expected_new_env_data.update({'type': 'local', 'is-default': True})
770 envs.set_env_data(self.env_db, None, expected_new_env_data)946 envs.set_env_data(self.env_db, None, expected_new_env_data)
771 mock_env_detail.assert_called_once_with(947 mock_env_detail.assert_called_once_with(
772 self.app, self.env_type_db, self.env_db, self.save_callable,948 self.app, self.env_type_db, self.env_db, self.jenv_db,
773 expected_new_env_data)949 self.save_callable, expected_new_env_data)
774950
775 def test_save_invalid_form_data(self):951 def test_save_invalid_form_data(self):
776 # Errors are displayed if the user tries to save invalid data.952 # Errors are displayed if the user tries to save invalid data.
@@ -815,7 +991,8 @@
815 cancel_button = self.get_control_buttons()[1]991 cancel_button = self.get_control_buttons()[1]
816 cli_helpers.emit(cancel_button)992 cli_helpers.emit(cancel_button)
817 mock_env_index.assert_called_once_with(993 mock_env_index.assert_called_once_with(
818 self.app, self.env_type_db, self.env_db, self.save_callable)994 self.app, self.env_type_db, self.env_db, self.jenv_db,
995 self.save_callable)
819996
820 @mock.patch('quickstart.cli.views.env_detail')997 @mock.patch('quickstart.cli.views.env_detail')
821 def test_modification_view_cancel_button(self, mock_env_detail):998 def test_modification_view_cancel_button(self, mock_env_detail):
@@ -826,5 +1003,5 @@
826 cancel_button = self.get_control_buttons()[1]1003 cancel_button = self.get_control_buttons()[1]
827 cli_helpers.emit(cancel_button)1004 cli_helpers.emit(cancel_button)
828 mock_env_detail.assert_called_once_with(1005 mock_env_detail.assert_called_once_with(
829 self.app, self.env_type_db, self.env_db, self.save_callable,1006 self.app, self.env_type_db, self.env_db, self.jenv_db,
830 self.env_data)1007 self.save_callable, self.env_data)
8311008
=== modified file 'quickstart/tests/helpers.py'
--- quickstart/tests/helpers.py 2014-12-16 16:16:04 +0000
+++ quickstart/tests/helpers.py 2014-12-17 12:34:18 +0000
@@ -262,9 +262,21 @@
262def make_jenv_db():262def make_jenv_db():
263 """Create and return a jenv files database."""263 """Create and return a jenv files database."""
264 environments = {264 environments = {
265 'ec2-west': {'type': '__unknown__'},265 'ec2-west': {
266 'lxc': {'type': 'local'},266 'type': '__unknown__',
267 'test-jenv': {'type': '__unknown__'},267 'user': 'who',
268 'state-servers': ('1.2.3.4:42', '1.2.3.4:47'),
269 },
270 'lxc': {
271 'type': 'local',
272 'user': 'dalek',
273 'state-servers': ('localhost:17070', '10.0.3.1:17070'),
274 },
275 'test-jenv': {
276 'type': '__unknown__',
277 'user': 'my-user',
278 'state-servers': ('10.0.3.1:17070',),
279 },
268 }280 }
269 return {'environments': environments}281 return {'environments': environments}
270282
271283
=== modified file 'quickstart/tests/models/test_jenv.py'
--- quickstart/tests/models/test_jenv.py 2014-12-16 16:04:16 +0000
+++ quickstart/tests/models/test_jenv.py 2014-12-17 12:34:18 +0000
@@ -200,9 +200,14 @@
200 'ec2': yaml.safe_dump(self.jenv_data),200 'ec2': yaml.safe_dump(self.jenv_data),
201 }):201 }):
202 jenv_db = jenv.get_env_db()202 jenv_db = jenv.get_env_db()
203 self.assertEqual({203 expected_environments = {
204 'environments': {'ec2': {'type': 'ec2'}},204 'ec2': {
205 }, jenv_db)205 'type': 'ec2',
206 'user': 'admin',
207 'state-servers': ('localhost:17070', '10.0.3.1:17070'),
208 },
209 }
210 self.assertEqual({'environments': expected_environments}, jenv_db)
206211
207 def test_multiple_jenv_files(self):212 def test_multiple_jenv_files(self):
208 # Multiple environments are correctly returned.213 # Multiple environments are correctly returned.
@@ -213,22 +218,29 @@
213 'bootstrap-config': {'type': 'hp'},218 'bootstrap-config': {'type': 'hp'},
214 }219 }
215 jenv_data2 = {220 jenv_data2 = {
216 'user': 'admin',221 'user': 'my-user',
217 'password': 'Secret!',222 'password': 'Secret!',
218 'state-servers': ['localhost:17070'],223 'state-servers': ['1.2.3.4:5', '1.2.3.4:42'],
219 'bootstrap-config': {'type': 'maas'},224 'bootstrap-config': {'type': 'maas'},
220 }225 }
221 with self.make_multiple_jenvs({226 with self.make_multiple_jenvs({
222 'hp': yaml.safe_dump(jenv_data1),227 'hp-cloud': yaml.safe_dump(jenv_data1),
223 'maas': yaml.safe_dump(jenv_data2),228 'maas': yaml.safe_dump(jenv_data2),
224 }):229 }):
225 jenv_db = jenv.get_env_db()230 jenv_db = jenv.get_env_db()
226 self.assertEqual({231 expected_environments = {
227 'environments': {232 'hp-cloud': {
228 'hp': {'type': 'hp'},233 'type': 'hp',
229 'maas': {'type': 'maas'},234 'user': 'admin',
230 },235 'state-servers': ('localhost:17070',),
231 }, jenv_db)236 },
237 'maas': {
238 'type': 'maas',
239 'user': 'my-user',
240 'state-servers': ('1.2.3.4:5', '1.2.3.4:42'),
241 },
242 }
243 self.assertEqual({'environments': expected_environments}, jenv_db)
232244
233 def test_unknown_env_type(self):245 def test_unknown_env_type(self):
234 # If the jenv file does not include the env type, jenv.UNKNOWN_ENV_TYPE246 # If the jenv file does not include the env type, jenv.UNKNOWN_ENV_TYPE
@@ -240,9 +252,14 @@
240 }252 }
241 with self.make_jenv('local', yaml.safe_dump(jenv_data)):253 with self.make_jenv('local', yaml.safe_dump(jenv_data)):
242 jenv_db = jenv.get_env_db()254 jenv_db = jenv.get_env_db()
243 self.assertEqual({255 expected_environments = {
244 'environments': {'local': {'type': jenv.UNKNOWN_ENV_TYPE}},256 'local': {
245 }, jenv_db)257 'type': jenv.UNKNOWN_ENV_TYPE,
258 'user': 'admin',
259 'state-servers': ('localhost:17070',),
260 },
261 }
262 self.assertEqual({'environments': expected_environments}, jenv_db)
246263
247 def test_extraneous_files(self):264 def test_extraneous_files(self):
248 # Extraneous files are ignored.265 # Extraneous files are ignored.
@@ -261,7 +278,9 @@
261278
262 def test_validation_success(self):279 def test_validation_success(self):
263 # A valid jenv file is successfully validated.280 # A valid jenv file is successfully validated.
264 jenv.validate(self.jenv_data)281 credentials, servers = jenv.validate(self.jenv_data)
282 self.assertEqual(('admin', 'Secret!'), credentials)
283 self.assertEqual(('localhost:17070', '10.0.3.1:17070'), servers)
265284
266 def test_invalid_credentials(self):285 def test_invalid_credentials(self):
267 # A ValueError is raised if the credentials cannot be retrieved.286 # A ValueError is raised if the credentials cannot be retrieved.
@@ -296,3 +315,54 @@
296 'password': 'Secret!',315 'password': 'Secret!',
297 'state-servers': [],316 'state-servers': [],
298 })317 })
318
319
320class TestGetEnvShortDescription(unittest.TestCase):
321
322 def test_env(self):
323 # The env description includes the environment name and type.
324 env_data = {'name': 'lxc', 'type': 'local'}
325 description = jenv.get_env_short_description(env_data)
326 self.assertEqual('lxc (type: local)', description)
327
328 def test_env_without_type(self):
329 # Without the type we can only show the environment name.
330 env_data = {'name': 'ec2', 'type': jenv.UNKNOWN_ENV_TYPE}
331 description = jenv.get_env_short_description(env_data)
332 self.assertEqual('ec2', description)
333
334
335class TestGetEnvDetails(unittest.TestCase):
336
337 def test_env(self):
338 # The environment details are properly returned.
339 env_data = {
340 'name': 'lxc',
341 'type': 'local',
342 'user': 'who',
343 'state-servers': ('1.2.3.4:17060', 'localhost:17070'),
344 }
345 expected_details = [
346 ('type', 'local'),
347 ('name', 'lxc'),
348 ('user', 'who'),
349 ('state servers', '1.2.3.4:17060, localhost:17070'),
350 ]
351 details = jenv.get_env_details(env_data)
352 self.assertEqual(expected_details, details)
353
354 def test_env_without_type(self):
355 # The environment type is not included if unknown.
356 env_data = {
357 'name': 'aws',
358 'type': jenv.UNKNOWN_ENV_TYPE,
359 'user': 'the-doctor',
360 'state-servers': ('1.2.3.4:17060',),
361 }
362 expected_details = [
363 ('name', 'aws'),
364 ('user', 'the-doctor'),
365 ('state servers', '1.2.3.4:17060'),
366 ]
367 details = jenv.get_env_details(env_data)
368 self.assertEqual(expected_details, details)
299369
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2014-12-16 16:04:16 +0000
+++ quickstart/tests/test_manage.py 2014-12-17 12:34:18 +0000
@@ -36,7 +36,10 @@
36 settings,36 settings,
37)37)
38from quickstart.cli import views38from quickstart.cli import views
39from quickstart.models import envs39from quickstart.models import (
40 envs,
41 jenv,
42)
40from quickstart.tests import helpers43from quickstart.tests import helpers
4144
4245
@@ -372,13 +375,15 @@
372 self.env_type_db = envs.get_env_type_db()375 self.env_type_db = envs.get_env_type_db()
373 self.env_file = self.make_env_file()376 self.env_file = self.make_env_file()
374 self.env_db = envs.load(self.env_file)377 self.env_db = envs.load(self.env_file)
378 self.jenv_db = helpers.make_jenv_db()
375379
376 @contextmanager380 @contextmanager
377 def patch_interactive_mode(self, env_db, return_value):381 def patch_interactive_mode(self, env_db, jenv_db, return_value):
378 """Patch the quickstart.cli.views.show function.382 """Patch the quickstart.cli.views.show function.
379383
380 Ensure the interactive mode is started by the code in the context block384 Ensure the interactive mode is started by the code in the context block
381 passing the given env_db. Make the view return the given return_value.385 passing the given env_db and jenv_db.
386 Make the view return the given return_value.
382 """387 """
383 create_save_callable_path = 'quickstart.manage._create_save_callable'388 create_save_callable_path = 'quickstart.manage._create_save_callable'
384 mock_show = mock.Mock(return_value=return_value)389 mock_show = mock.Mock(return_value=return_value)
@@ -387,16 +392,18 @@
387 yield392 yield
388 mock_save_callable.assert_called_once_with(self.parser, self.env_file)393 mock_save_callable.assert_called_once_with(self.parser, self.env_file)
389 mock_show.assert_called_once_with(394 mock_show.assert_called_once_with(
390 views.env_index, self.env_type_db, env_db,395 views.env_index, self.env_type_db, env_db, jenv_db,
391 mock_save_callable())396 mock_save_callable())
392397
393 def test_resulting_env_data(self):398 def test_resulting_env_data(self):
394 # The interactive session can be used to select an environment, in399 # The interactive session can be used to select an environment, in
395 # which case the function returns the corresponding env_data.400 # which case the function returns the corresponding env_data.
396 env_data = envs.get_env_data(self.env_db, 'aws')401 env_data = envs.get_env_data(self.env_db, 'aws')
397 with self.patch_interactive_mode(self.env_db, [self.env_db, env_data]):402 with self.patch_interactive_mode(
403 self.env_db, self.jenv_db, [self.env_db, env_data]):
398 obtained_env_data = manage._start_interactive_session(404 obtained_env_data = manage._start_interactive_session(
399 self.parser, self.env_type_db, self.env_db, self.env_file)405 self.parser, self.env_type_db, self.env_db, self.jenv_db,
406 self.env_file)
400 self.assertEqual(env_data, obtained_env_data)407 self.assertEqual(env_data, obtained_env_data)
401408
402 @helpers.mock_print409 @helpers.mock_print
@@ -405,9 +412,11 @@
405 # during the interactive session.412 # during the interactive session.
406 env_data = envs.get_env_data(self.env_db, 'aws')413 env_data = envs.get_env_data(self.env_db, 'aws')
407 new_env_db = helpers.make_env_db()414 new_env_db = helpers.make_env_db()
408 with self.patch_interactive_mode(self.env_db, [new_env_db, env_data]):415 with self.patch_interactive_mode(
416 self.env_db, self.jenv_db, [new_env_db, env_data]):
409 manage._start_interactive_session(417 manage._start_interactive_session(
410 self.parser, self.env_type_db, self.env_db, self.env_file)418 self.parser, self.env_type_db, self.env_db, self.jenv_db,
419 self.env_file)
411 mock_print.assert_called_once_with(420 mock_print.assert_called_once_with(
412 'changes to the environments file have been saved')421 'changes to the environments file have been saved')
413422
@@ -415,9 +424,11 @@
415 def test_interactive_mode_quit(self, mock_exit):424 def test_interactive_mode_quit(self, mock_exit):
416 # If the user explicitly quits the interactive mode, the program exits425 # If the user explicitly quits the interactive mode, the program exits
417 # without proceeding with the environment bootstrapping.426 # without proceeding with the environment bootstrapping.
418 with self.patch_interactive_mode(self.env_db, [self.env_db, None]):427 with self.patch_interactive_mode(
428 self.env_db, self.jenv_db, [self.env_db, None]):
419 manage._start_interactive_session(429 manage._start_interactive_session(
420 self.parser, self.env_type_db, self.env_db, self.env_file)430 self.parser, self.env_type_db, self.env_db, self.jenv_db,
431 self.env_file)
421 mock_exit.assert_called_once_with('quitting')432 mock_exit.assert_called_once_with('quitting')
422433
423434
@@ -488,7 +499,9 @@
488 self.assertIsNone(result)499 self.assertIsNone(result)
489500
490501
491class TestSetupEnv(helpers.EnvFileTestsMixin, unittest.TestCase):502class TestSetupEnv(
503 helpers.EnvFileTestsMixin, helpers.JenvFileTestsMixin,
504 unittest.TestCase):
492505
493 def setUp(self):506 def setUp(self):
494 self.parser = mock.Mock()507 self.parser = mock.Mock()
@@ -596,10 +609,12 @@
596 env_data = envs.get_env_data(env_db, 'aws')609 env_data = envs.get_env_data(env_db, 'aws')
597 get_env_type_db_path = 'quickstart.models.envs.get_env_type_db'610 get_env_type_db_path = 'quickstart.models.envs.get_env_type_db'
598 with mock.patch(get_env_type_db_path) as mock_get_env_type_db:611 with mock.patch(get_env_type_db_path) as mock_get_env_type_db:
599 with self.patch_interactive_mode(env_data) as mock_interactive:612 with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)):
600 manage._setup_env(options, self.parser)613 jenv_db = jenv.get_env_db()
614 with self.patch_interactive_mode(env_data) as mock_interactive:
615 manage._setup_env(options, self.parser)
601 mock_interactive.assert_called_once_with(616 mock_interactive.assert_called_once_with(
602 self.parser, mock_get_env_type_db(), env_db, env_file)617 self.parser, mock_get_env_type_db(), env_db, jenv_db, env_file)
603 # The options is updated with data from the selected environment.618 # The options is updated with data from the selected environment.
604 self.assertEqual(env_file, options.env_file)619 self.assertEqual(env_file, options.env_file)
605 self.assertEqual('aws', options.env_name)620 self.assertEqual('aws', options.env_name)

Subscribers

People subscribed via source and target branches