Merge lp:~frankban/juju-quickstart/remove-jenv into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 113
Proposed branch: lp:~frankban/juju-quickstart/remove-jenv
Merge into: lp:juju-quickstart
Diff against target: 422 lines (+200/-38)
8 files modified
quickstart/cli/params.py (+8/-1)
quickstart/cli/views.py (+30/-2)
quickstart/manage.py (+1/-0)
quickstart/models/jenv.py (+14/-0)
quickstart/tests/cli/test_params.py (+9/-2)
quickstart/tests/cli/test_views.py (+102/-33)
quickstart/tests/models/test_jenv.py (+35/-0)
quickstart/tests/test_manage.py (+1/-0)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/remove-jenv
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+246162@code.launchpad.net

Description of the change

Jenv files removal functionality.

Add the ability to remove jenv files
from the Juju home.

Test: `make check`.

QA:
- `juju bootstrap` an environment;
- create a new user for the environment:
  `juju user add myuser --generate -o ~/.juju/environments/myenv`;
- run quickstart in interactive mode:
  `.venv/bin/python juju-quickstart -i`;
- the "myenv" environment should be listed under
  "Other active environments": select it;
- ensure the corresponding environment description in
  the jenv detail view makes sense;
- click to remove the environment, cancel the removal in the
  confirm dialog and ensure the environment is still there;
- click the "remove" button again, confirm the deletion and
  ensure that a message is displayed and the environment
  file has been deleted. Also quickstart redirects to the
  environments index view and "myenv" is no longer there;
- destroy the environment: done, thank you!

https://codereview.appspot.com/189540043/

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

Reviewers: mp+246162_code.launchpad.net,

Message:
Please take a look.

Description:
Jenv files removal functionality.

Add the ability to remove jenv files
from the Juju home.

Test: `make check`.

QA:
- `juju bootstrap` an environment;
- create a new user for the environment:
   `juju user add myuser --generate -o ~/.juju/environments/myenv`;
- run quickstart in interactive mode:
   `.venv/bin/python juju-quickstart -i`;
- the "myenv" environment should be listed under
   "Other active environments": select it;
- ensure the corresponding environment description in
   the jenv detail view makes sense;
- click to remove the environment, cancel the removal in the
   confirm dialog and ensure the environment is still there;
- click the "remove" button again, confirm the deletion and
   ensure that a message is displayed and the environment
   file has been deleted. Also quickstart redirects to the
   environments index view and "myenv" is no longer there;
- destroy the environment: done, thank you!

https://code.launchpad.net/~frankban/juju-quickstart/remove-jenv/+merge/246162

(do not edit description out of merge proposal)

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

Affected files (+200, -38 lines):
   A [revision details]
   M quickstart/cli/params.py
   M quickstart/cli/views.py
   M quickstart/manage.py
   M quickstart/models/jenv.py
   M quickstart/tests/cli/test_params.py
   M quickstart/tests/cli/test_views.py
   M quickstart/tests/models/test_jenv.py
   M quickstart/tests/test_manage.py

Revision history for this message
Martin Hilton (martin-hilton) wrote :

LGTM No QA yet, will do so shortly

https://codereview.appspot.com/189540043/diff/1/quickstart/cli/views.py
File quickstart/cli/views.py (right):

https://codereview.appspot.com/189540043/diff/1/quickstart/cli/views.py#newcode528
quickstart/cli/views.py:528: '\nFor this reason, it is not possible to
edit/change it.\n'
Isn't edit/change a tautology? Wouldn't "edit it." be enough.

https://codereview.appspot.com/189540043/

Revision history for this message
Brad Crittenden (bac) wrote :

The code LGTM but I question the need for the functionality in
quickstart. I don't feel strongly enough to abandon the work but I think
it only marginally improves the user experience at the expense of more
complication. Perhaps I'm missing a use-case that warrants the
function.

https://codereview.appspot.com/189540043/diff/1/quickstart/cli/params.py
File quickstart/cli/params.py (right):

https://codereview.appspot.com/189540043/diff/1/quickstart/cli/params.py#newcode37
quickstart/cli/params.py:37: )
Formatting is odd. I'd prefer one per line if you're doing multi-line.

https://codereview.appspot.com/189540043/

115. By Francesco Banconi

Fix typo and improve params formatting.

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

Please take a look.

https://codereview.appspot.com/189540043/diff/1/quickstart/cli/params.py
File quickstart/cli/params.py (right):

https://codereview.appspot.com/189540043/diff/1/quickstart/cli/params.py#newcode37
quickstart/cli/params.py:37: )
On 2015/01/12 15:29:32, bac wrote:
> Formatting is odd. I'd prefer one per line if you're doing
multi-line.

Done.

https://codereview.appspot.com/189540043/diff/1/quickstart/cli/views.py
File quickstart/cli/views.py (right):

https://codereview.appspot.com/189540043/diff/1/quickstart/cli/views.py#newcode528
quickstart/cli/views.py:528: '\nFor this reason, it is not possible to
edit/change it.\n'
On 2015/01/12 14:50:22, martin.hilton wrote:
> Isn't edit/change a tautology? Wouldn't "edit it." be enough.

Done.

https://codereview.appspot.com/189540043/

Revision history for this message
Martin Hilton (martin-hilton) wrote :

QA OK following instructions.

https://codereview.appspot.com/189540043/

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

*** Submitted:

Jenv files removal functionality.

Add the ability to remove jenv files
from the Juju home.

Test: `make check`.

QA:
- `juju bootstrap` an environment;
- create a new user for the environment:
   `juju user add myuser --generate -o ~/.juju/environments/myenv`;
- run quickstart in interactive mode:
   `.venv/bin/python juju-quickstart -i`;
- the "myenv" environment should be listed under
   "Other active environments": select it;
- ensure the corresponding environment description in
   the jenv detail view makes sense;
- click to remove the environment, cancel the removal in the
   confirm dialog and ensure the environment is still there;
- click the "remove" button again, confirm the deletion and
   ensure that a message is displayed and the environment
   file has been deleted. Also quickstart redirects to the
   environments index view and "myenv" is no longer there;
- destroy the environment: done, thank you!

R=martin.hilton, bac
CC=
https://codereview.appspot.com/189540043

https://codereview.appspot.com/189540043/

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/cli/params.py'
2--- quickstart/cli/params.py 2015-01-07 15:17:49 +0000
3+++ quickstart/cli/params.py 2015-01-12 15:39:31 +0000
4@@ -30,7 +30,13 @@
5 import copy
6
7
8-_PARAMS = ('env_type_db', 'env_db', 'jenv_db', 'save_callable')
9+_PARAMS = (
10+ 'env_type_db',
11+ 'env_db',
12+ 'jenv_db',
13+ 'save_callable',
14+ 'remove_jenv_callable',
15+)
16
17
18 class Params(namedtuple('Params', _PARAMS)):
19@@ -48,4 +54,5 @@
20 env_db=copy.deepcopy(self.env_db),
21 jenv_db=copy.deepcopy(self.jenv_db),
22 save_callable=self.save_callable,
23+ remove_jenv_callable=self.remove_jenv_callable,
24 )
25
26=== modified file 'quickstart/cli/views.py'
27--- quickstart/cli/views.py 2015-01-08 14:53:06 +0000
28+++ quickstart/cli/views.py 2015-01-12 15:39:31 +0000
29@@ -525,16 +525,26 @@
30 urwid.Text([
31 ('highlight', 'Imported active environment.\n'),
32 'This environment is not included in your environments.yaml file.'
33- '\nFor this reason, it is not possible to edit or remove it.\n'
34+ '\nFor this reason, it is not possible to edit it.\n'
35 'However, you can use the link below to ',
36 ('highlight', 'use Juju Quickstart'),
37- ' with this environment.',
38+ ' with this environment or ',
39+ ('highlight', 'remove the corresponding jenv file'),
40+ '.\n'
41+ 'Note that removing the Juju generated environment file does not '
42+ 'destroy the corresponding active environment.'
43 ]),
44 ])
45
46+ remove_callback = ui.thunk(
47+ _remove_jenv, params.jenv_db, env_data, params.remove_jenv_callable,
48+ app.set_message, index_view)
49+ confirm_removal_callback = ui.thunk(
50+ _confirm_removal, app, env_data, remove_callback)
51 controls = [
52 ui.MenuButton('back', ui.thunk(index_view)),
53 ui.MenuButton('use', ui.thunk(_use, params.env_db, env_data)),
54+ ui.MenuButton(('control alert', 'remove'), confirm_removal_callback),
55 ]
56 widgets.append(ui.create_controls(*controls))
57 listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
58@@ -542,6 +552,24 @@
59 app.set_status([' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate '])
60
61
62+def _remove_jenv(
63+ jenv_db, env_data, remove_jenv_callable, set_message, redirect_view):
64+ """Remove the jenv file corresonding to the env_data environment.
65+
66+ Update the provided jenv_db and return to the given view.
67+ Also output a notification using the given set_message callable.
68+ """
69+ env_name = env_data['name']
70+ # Remove the jenv file.
71+ msg = remove_jenv_callable(env_name)
72+ if msg is None:
73+ msg = '{} successfully removed'.format(env_name)
74+ # Also remove the environments from the jenv database.
75+ del jenv_db['environments'][env_name]
76+ set_message(msg)
77+ redirect_view()
78+
79+
80 def env_edit(app, params, env_data):
81 """Create or modify a Juju environment.
82
83
84=== modified file 'quickstart/manage.py'
85--- quickstart/manage.py 2015-01-07 14:07:22 +0000
86+++ quickstart/manage.py 2015-01-12 15:39:31 +0000
87@@ -218,6 +218,7 @@
88 env_db=env_db,
89 jenv_db=jenv_db,
90 save_callable=_create_save_callable(parser, env_file),
91+ remove_jenv_callable=jenv.remove,
92 )
93 new_env_db, env_data = views.show(views.env_index, parameters)
94 if new_env_db != env_db:
95
96=== modified file 'quickstart/models/jenv.py'
97--- quickstart/models/jenv.py 2014-12-17 12:28:36 +0000
98+++ quickstart/models/jenv.py 2015-01-12 15:39:31 +0000
99@@ -174,6 +174,20 @@
100 return db
101
102
103+def remove(env_name):
104+ """Remove the jenv file corresponding to the given environment name.
105+
106+ Return None if the removal was successful, an error message otherwise.
107+ """
108+ jenv_path = _get_jenv_path(env_name)
109+ try:
110+ os.remove(jenv_path)
111+ except OSError as err:
112+ msg = 'cannot remove the {} environment: {}'
113+ return msg.format(env_name, bytes(err).decode('utf-8'))
114+ return None
115+
116+
117 def validate(data):
118 """Validate the given YAML decoded jenv data.
119
120
121=== modified file 'quickstart/tests/cli/test_params.py'
122--- quickstart/tests/cli/test_params.py 2015-01-07 14:07:22 +0000
123+++ quickstart/tests/cli/test_params.py 2015-01-12 15:39:31 +0000
124@@ -33,21 +33,24 @@
125 self.env_db = helpers.make_env_db()
126 self.jenv_db = helpers.make_jenv_db()
127 self.save_callable = lambda env_db: None
128+ self.remove_jenv_callable = lambda env_db: None
129 # Set up a params object used in tests.
130 self.params = params.Params(
131 env_type_db=self.env_type_db,
132 env_db=self.env_db,
133 jenv_db=self.jenv_db,
134 save_callable=self.save_callable,
135+ remove_jenv_callable=self.remove_jenv_callable,
136 )
137
138 def test_tuple(self):
139 # The params object can be used as a tuple.
140- env_type_db, env_db, jenv_db, save_callable = self.params
141+ env_type_db, env_db, jenv_db, save, remove = self.params
142 self.assertIs(self.env_type_db, env_type_db)
143 self.assertIs(self.env_db, env_db)
144 self.assertIs(self.jenv_db, jenv_db)
145- self.assertIs(self.save_callable, save_callable)
146+ self.assertIs(self.save_callable, save)
147+ self.assertIs(self.remove_jenv_callable, remove)
148
149 def test_attributes(self):
150 # Parameters can be accessed as attributes.
151@@ -55,6 +58,8 @@
152 self.assertIs(self.env_db, self.params.env_db)
153 self.assertIs(self.jenv_db, self.params.jenv_db)
154 self.assertIs(self.save_callable, self.params.save_callable)
155+ self.assertIs(
156+ self.remove_jenv_callable, self.params.remove_jenv_callable)
157
158 def test_immutable(self):
159 # It is not possible to replace a stored parameter.
160@@ -69,6 +74,8 @@
161 self.assertIs(self.env_db, self.params.env_db)
162 self.assertIs(self.jenv_db, self.params.jenv_db)
163 self.assertIs(self.save_callable, self.params.save_callable)
164+ self.assertIs(
165+ self.remove_jenv_callable, self.params.remove_jenv_callable)
166 # The new params object stores the same data.
167 self.assertEqual(self.params, params)
168 # But they do not refer to the same object.
169
170=== modified file 'quickstart/tests/cli/test_views.py'
171--- quickstart/tests/cli/test_views.py 2015-01-07 15:36:57 +0000
172+++ quickstart/tests/cli/test_views.py 2015-01-12 15:39:31 +0000
173@@ -137,6 +137,7 @@
174 # Set up the base Urwid application.
175 self.loop, self.app = base.setup_urwid_app()
176 self.save_callable = mock.Mock()
177+ self.remove_jenv_callable = mock.Mock(return_value=None)
178
179 def get_widgets_in_contents(self, filter_function=None):
180 """Return a list of widgets included in the app contents.
181@@ -174,8 +175,45 @@
182 env_db=env_db,
183 jenv_db=jenv_db,
184 save_callable=self.save_callable,
185+ remove_jenv_callable=self.remove_jenv_callable,
186 )
187
188+ def click_remove_button(self, env_name):
189+ """Click the remove button in an environment detail view.
190+
191+ Assume the view was already called.
192+ Return the dialog button widgets and the original view contents.
193+ """
194+ original_contents = self.app.get_contents()
195+ # The "remove" button is the last one.
196+ remove_button = self.get_control_buttons()[-1]
197+ cli_helpers.emit(remove_button)
198+ # The original env detail contents have been replaced.
199+ contents = self.app.get_contents()
200+ self.assertIsNot(contents, original_contents)
201+ # A "remove" confirmation dialog is displayed.
202+ title_widget, message_widget, buttons = cli_helpers.inspect_dialog(
203+ contents)
204+ self.assertEqual(
205+ 'Remove the {} environment'.format(env_name), title_widget.text)
206+ self.assertEqual('This action cannot be undone!', message_widget.text)
207+ return buttons, original_contents
208+
209+ def cancel_removal(self, env_name):
210+ """Cancel the environment deletion by clicking the "cancel" button."""
211+ buttons, original_contents = self.click_remove_button(env_name)
212+ # The "cancel" button is the first one in the dialog.
213+ cancel_button = buttons[0]
214+ cli_helpers.emit(cancel_button)
215+ return original_contents
216+
217+ def confirm_removal(self, env_name):
218+ """Confirm the environment deletion."""
219+ buttons, _ = self.click_remove_button(env_name)
220+ # The "confirm" button is the second one in the dialog.
221+ confirm_button = buttons[1]
222+ cli_helpers.emit(confirm_button)
223+
224
225 class TestEnvIndex(EnvViewTestsMixin, unittest.TestCase):
226
227@@ -622,18 +660,7 @@
228 def test_remove_button(self):
229 # A confirmation dialog is displayed if the "remove" button is clicked.
230 self.call_view(env_name='ec2-west')
231- original_contents = self.app.get_contents()
232- # The "remove" button is the last one.
233- remove_button = self.get_control_buttons()[-1]
234- cli_helpers.emit(remove_button)
235- # The original env detail contents have been replaced.
236- contents = self.app.get_contents()
237- self.assertIsNot(contents, original_contents)
238- # A "remove" confirmation dialog is displayed.
239- title_widget, message_widget, buttons = cli_helpers.inspect_dialog(
240- contents)
241- self.assertEqual('Remove the ec2-west environment', title_widget.text)
242- self.assertEqual('This action cannot be undone!', message_widget.text)
243+ buttons, _ = self.click_remove_button('ec2-west')
244 # The dialog includes the "cancel" and "confirm" buttons.
245 self.assertEqual(2, len(buttons))
246 captions = map(cli_helpers.get_button_caption, buttons)
247@@ -642,15 +669,7 @@
248 def test_remove_cancelled(self):
249 # The "remove" confirmation dialog can be safely dismissed.
250 self.call_view(env_name='ec2-west')
251- original_contents = self.app.get_contents()
252- # The "remove" button is the last one.
253- remove_button = self.get_control_buttons()[-1]
254- cli_helpers.emit(remove_button)
255- contents = self.app.get_contents()
256- buttons = cli_helpers.inspect_dialog(contents)[2]
257- # The "cancel" button is the first one in the dialog.
258- cancel_button = buttons[0]
259- cli_helpers.emit(cancel_button)
260+ original_contents = self.cancel_removal('ec2-west')
261 # The original contents have been restored.
262 self.assertIs(original_contents, self.app.get_contents())
263
264@@ -659,20 +678,17 @@
265 # The current environment is removed if the "remove" button is clicked
266 # and then the deletion is confirmed. Subsequently the application
267 # switches to the index view.
268- self.call_view(env_name='ec2-west')
269- # The "remove" button is the last one.
270- remove_button = self.get_control_buttons()[-1]
271- cli_helpers.emit(remove_button)
272- contents = self.app.get_contents()
273- buttons = cli_helpers.inspect_dialog(contents)[2]
274- # The "confirm" button is the second one in the dialog.
275- confirm_button = buttons[1]
276- cli_helpers.emit(confirm_button)
277+ env_name = 'ec2-west'
278+ self.call_view(env_name=env_name)
279+ self.confirm_removal(env_name)
280+ # A message notifies the environment has been removed.
281+ self.assertEqual(
282+ '{} successfully removed'.format(env_name), self.app.get_message())
283 # The index view has been called passing the modified env_db in params.
284 self.assertTrue(mock_env_index.called)
285 params = mock_env_index.call_args[0][1]
286 # The new env_db no longer includes the "ec2-west" environment.
287- self.assertNotIn('ec2-west', params.env_db['environments'])
288+ self.assertNotIn(env_name, params.env_db['environments'])
289 # The new env_db has been saved.
290 self.save_callable.assert_called_once_with(params.env_db)
291
292@@ -732,11 +748,11 @@
293 self.assertEqual(expected_text, widget.text)
294
295 def test_view_buttons(self):
296- # The "back" and "use" buttons are displayed.
297+ # The "back", "use" and "remove" buttons are displayed.
298 self.call_view(env_name='ec2-west')
299 buttons = self.get_control_buttons()
300 captions = map(cli_helpers.get_button_caption, buttons)
301- self.assertEqual(['back', 'use'], captions)
302+ self.assertEqual(['back', 'use', 'remove'], captions)
303
304 @mock.patch('quickstart.cli.views.env_index')
305 def test_back_button(self, mock_env_index):
306@@ -759,6 +775,59 @@
307 self.assertEqual(
308 expected_return_value, context_manager.exception.return_value)
309
310+ def test_remove_button(self):
311+ # A confirmation dialog is displayed if the "remove" button is clicked.
312+ self.call_view(env_name='test-jenv')
313+ buttons, _ = self.click_remove_button('test-jenv')
314+ # The dialog includes the "cancel" and "confirm" buttons.
315+ self.assertEqual(2, len(buttons))
316+ captions = map(cli_helpers.get_button_caption, buttons)
317+ self.assertEqual(['cancel', 'confirm'], captions)
318+
319+ def test_remove_cancelled(self):
320+ # The "remove" confirmation dialog can be safely dismissed.
321+ self.call_view(env_name='test-jenv')
322+ original_contents = self.cancel_removal('test-jenv')
323+ # The original contents have been restored.
324+ self.assertIs(original_contents, self.app.get_contents())
325+
326+ @mock.patch('quickstart.cli.views.env_index')
327+ def test_remove_confirmed(self, mock_env_index):
328+ # The jenv file is removed if the "remove" button is clicked and then
329+ # then the deletion is confirmed. Subsequently the application switches
330+ # to the index view.
331+ env_name = 'test-jenv'
332+ self.call_view(env_name=env_name)
333+ self.confirm_removal(env_name)
334+ # A message notifies the environment has been removed.
335+ self.assertEqual(
336+ '{} successfully removed'.format(env_name), self.app.get_message())
337+ # The index view has been called passing the modified jenv_db params.
338+ self.assertTrue(mock_env_index.called)
339+ params = mock_env_index.call_args[0][1]
340+ # The new jenv_db no longer includes the "test-jenv" environment.
341+ self.assertNotIn(env_name, params.jenv_db['environments'])
342+ # The corresponding jenv file has been removed.
343+ self.remove_jenv_callable.assert_called_once_with(env_name)
344+ self.assertEqual(
345+ 'test-jenv successfully removed', self.app.get_message())
346+
347+ @mock.patch('quickstart.cli.views.env_index')
348+ def test_remove_confirmed_error(self, mock_env_index):
349+ # Errors occurred while trying to remove the jenv files are notified.
350+ env_name = 'test-jenv'
351+ self.call_view(env_name=env_name)
352+ # Simulate an error removing the jenv file.
353+ self.remove_jenv_callable.return_value = 'bad wolf'
354+ self.confirm_removal(env_name)
355+ # The error is notified.
356+ self.assertEqual('bad wolf'.format(env_name), self.app.get_message())
357+ # The index view has been called passing the original jenv_db params.
358+ self.assertTrue(mock_env_index.called)
359+ params = mock_env_index.call_args[0][1]
360+ # The jenv_db still includes the "test_jenv" environment.
361+ self.assertIn(env_name, params.jenv_db['environments'])
362+
363
364 class TestEnvEdit(EnvViewTestsMixin, unittest.TestCase):
365
366
367=== modified file 'quickstart/tests/models/test_jenv.py'
368--- quickstart/tests/models/test_jenv.py 2014-12-17 11:34:06 +0000
369+++ quickstart/tests/models/test_jenv.py 2015-01-12 15:39:31 +0000
370@@ -272,6 +272,41 @@
371 self.assertEqual({'environments': {}}, jenv_db)
372
373
374+class TestRemove(helpers.JenvFileTestsMixin, unittest.TestCase):
375+
376+ @classmethod
377+ def setUpClass(cls):
378+ # Prepare the jenv file contents.
379+ cls.contents = yaml.safe_dump(cls.jenv_data)
380+
381+ def test_successful_removal(self):
382+ # The jenv file is correctly removed.
383+ with self.make_jenv('local', self.contents) as path:
384+ error = jenv.remove('local')
385+ self.assertIsNone(error)
386+ self.assertFalse(os.path.exists(path))
387+
388+ def test_error_directory(self):
389+ # An error message is returned if the jenv path points to a directory.
390+ with self.make_jenv('local', self.contents) as path:
391+ dirname = os.path.dirname(path)
392+ os.mkdir(os.path.join(dirname, 'ec2.jenv'))
393+ error = jenv.remove('ec2')
394+ expected_error = (
395+ 'cannot remove the ec2 environment: '
396+ '[Errno 21] Is a directory: ')
397+ self.assertIn(expected_error, error)
398+
399+ def test_error_not_found(self):
400+ # An error is returned if the environment cannot be found.
401+ with self.make_jenv('local', self.contents):
402+ error = jenv.remove('hp')
403+ expected_error = (
404+ 'cannot remove the hp environment: '
405+ '[Errno 2] No such file or directory: ')
406+ self.assertIn(expected_error, error)
407+
408+
409 class TestValidate(
410 helpers.JenvFileTestsMixin, helpers.ValueErrorTestsMixin,
411 unittest.TestCase):
412
413=== modified file 'quickstart/tests/test_manage.py'
414--- quickstart/tests/test_manage.py 2015-01-07 14:07:22 +0000
415+++ quickstart/tests/test_manage.py 2015-01-12 15:39:31 +0000
416@@ -399,6 +399,7 @@
417 env_db=env_db,
418 jenv_db=jenv_db,
419 save_callable=mock_save_callable(),
420+ remove_jenv_callable=jenv.remove,
421 )
422 mock_show.assert_called_once_with(views.env_index, expected_params)
423

Subscribers

People subscribed via source and target branches