Skip to content

Commit 2d233e4

Browse files
committed
Implemented Wizard delegation and unit tests
Improved error handler test cases
1 parent c719f89 commit 2d233e4

File tree

4 files changed

+91
-30
lines changed

4 files changed

+91
-30
lines changed

awsshell/app.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,12 @@ def run(self, command, application):
153153

154154

155155
class WizardHandler(object):
156-
def __init__(self, output=sys.stdout, err=sys.stderr,
157-
loader=WizardLoader()):
156+
def __init__(self, output=sys.stdout, err=sys.stderr, loader=None):
158157
self._output = output
159158
self._err = err
160159
self._wizard_loader = loader
160+
if self._wizard_loader is None:
161+
self._wizard_loader = WizardLoader()
161162

162163
def run(self, command, application):
163164
"""Run the specified wizard.

awsshell/wizard.py

+31-19
Original file line numberDiff line numberDiff line change
@@ -111,20 +111,21 @@ def create_wizard(self, model):
111111
stages = self._load_stages(model.get('Stages'), env)
112112
return Wizard(start_stage, stages, env, self._error_handler)
113113

114+
def _load_stage(self, stage, env):
115+
stage_attrs = {
116+
'name': stage.get('Name'),
117+
'prompt': stage.get('Prompt'),
118+
'retrieval': stage.get('Retrieval'),
119+
'next_stage': stage.get('NextStage'),
120+
'resolution': stage.get('Resolution'),
121+
'interaction': stage.get('Interaction'),
122+
}
123+
creator = self._cached_creator
124+
interaction = self._interaction_loader
125+
return Stage(env, creator, interaction, self, **stage_attrs)
126+
114127
def _load_stages(self, stages, env):
115-
def load_stage(stage):
116-
stage_attrs = {
117-
'name': stage.get('Name'),
118-
'prompt': stage.get('Prompt'),
119-
'retrieval': stage.get('Retrieval'),
120-
'next_stage': stage.get('NextStage'),
121-
'resolution': stage.get('Resolution'),
122-
'interaction': stage.get('Interaction'),
123-
}
124-
creator = self._cached_creator
125-
loader = self._interaction_loader
126-
return Stage(env, creator, loader, **stage_attrs)
127-
return [load_stage(stage) for stage in stages]
128+
return [self._load_stage(stage, env) for stage in stages]
128129

129130

130131
class Wizard(object):
@@ -177,8 +178,10 @@ def execute(self):
177178
raise WizardException('Stage not found: %s' % current_stage)
178179
try:
179180
self._push_stage(stage)
180-
stage.execute()
181+
stage_data = stage.execute()
181182
current_stage = stage.get_next_stage()
183+
if current_stage is None:
184+
return stage_data
182185
except Exception as err:
183186
stages = [s.name for (s, _) in self._stage_history]
184187
recovery = self._error_handler(err, stages)
@@ -199,9 +202,9 @@ def _pop_stages(self, stage_index):
199202
class Stage(object):
200203
"""The Stage object. Contains logic to run all steps of the stage."""
201204

202-
def __init__(self, env, creator, interaction_loader, name=None,
203-
prompt=None, retrieval=None, next_stage=None, resolution=None,
204-
interaction=None):
205+
def __init__(self, env, creator, interaction_loader, wizard_loader,
206+
name=None, prompt=None, retrieval=None, next_stage=None,
207+
resolution=None, interaction=None):
205208
"""Construct a new Stage object.
206209
207210
:type env: :class:`Environment`
@@ -235,6 +238,7 @@ def __init__(self, env, creator, interaction_loader, name=None,
235238
"""
236239
self._env = env
237240
self._cached_creator = creator
241+
self._wizard_loader = wizard_loader
238242
self._interaction_loader = interaction_loader
239243
self.name = name
240244
self.prompt = prompt
@@ -270,6 +274,11 @@ def _handle_request_retrieval(self):
270274
# execute operation passing all parameters
271275
return operation(**parameters)
272276

277+
def _handle_wizard_delegation(self):
278+
wizard_name = self.retrieval['Resource']
279+
wizard = self._wizard_loader.load_wizard(wizard_name)
280+
return wizard.execute()
281+
273282
def _handle_retrieval(self):
274283
# In case of no retrieval, empty dict
275284
if not self.retrieval:
@@ -278,14 +287,15 @@ def _handle_retrieval(self):
278287
data = self._handle_static_retrieval()
279288
elif self.retrieval['Type'] == 'Request':
280289
data = self._handle_request_retrieval()
290+
elif self.retrieval['Type'] == 'Wizard':
291+
data = self._handle_wizard_delegation()
281292
# Apply JMESPath query if given
282293
if self.retrieval.get('Path'):
283294
data = jmespath.search(self.retrieval['Path'], data)
284295

285296
return data
286297

287298
def _handle_interaction(self, data):
288-
289299
# if no interaction step, just forward data
290300
if self.interaction is None:
291301
return data
@@ -299,6 +309,7 @@ def _handle_resolution(self, data):
299309
if self.resolution.get('Path'):
300310
data = jmespath.search(self.resolution['Path'], data)
301311
self._env.store(self.resolution['Key'], data)
312+
return data
302313

303314
def get_next_stage(self):
304315
"""Resolve the next stage name for the stage after this one.
@@ -322,7 +333,8 @@ def execute(self):
322333
"""
323334
retrieved_options = self._handle_retrieval()
324335
selected_data = self._handle_interaction(retrieved_options)
325-
self._handle_resolution(selected_data)
336+
resolved_data = self._handle_resolution(selected_data)
337+
return resolved_data
326338

327339

328340
class Environment(object):

tests/unit/test_app.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,11 @@ def test_exit_dot_command_exits_shell():
191191
assert mock_prompter.run.call_count == 1
192192

193193

194-
def test_wizard_can_load_and_execute():
194+
def test_wizard_can_load_and_execute(errstream):
195195
# Proper dot command syntax should load and run a wizard
196196
mock_loader = mock.Mock()
197197
mock_wizard = mock_loader.load_wizard.return_value
198+
mock_wizard.execute.return_value = {}
198199
handler = app.WizardHandler(err=errstream, loader=mock_loader)
199200
handler.run(['.wizard', 'wizname'], None)
200201

tests/unit/test_wizard.py

+55-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import mock
22
import pytest
3+
import botocore.session
4+
5+
from botocore.loaders import Loader
36
from botocore.session import Session
47
from awsshell.utils import FileReadError
58
from awsshell.wizard import stage_error_handler
@@ -200,20 +203,20 @@ def test_basic_full_execution(wizard_spec, loader):
200203
def test_basic_full_execution_error(wizard_spec):
201204
# Test that the wizard can handle exceptions in stage execution
202205
session = mock.Mock()
203-
error_handler = mock.Mock()
204-
error_handler.return_value = ('TestStage', 0)
206+
error_handler = mock.Mock(side_effect=[('TestStage', 0), None])
205207
loader = WizardLoader(session, error_handler=error_handler)
206208
wizard_spec['Stages'][0]['NextStage'] = \
207209
{'Type': 'Name', 'Name': 'StageTwo'}
208210
wizard_spec['Stages'][0]['Resolution']['Path'] = '[0].Stage'
209211
stage_three = {'Name': 'StageThree', 'Prompt': 'Text'}
210212
wizard = loader.create_wizard(wizard_spec)
211-
# force an exception once, let it recover, re-run
212-
error = WizardException()
213-
wizard.stages['StageTwo'].execute = mock.Mock(side_effect=[error, {}])
214-
wizard.execute()
215-
# assert error handler was called
216-
assert error_handler.call_count == 1
213+
# force two exceptions, recover once then fail to recover
214+
errors = [WizardException(), TypeError()]
215+
wizard.stages['StageTwo'].execute = mock.Mock(side_effect=errors)
216+
with pytest.raises(TypeError):
217+
wizard.execute()
218+
# assert error handler was called twice
219+
assert error_handler.call_count == 2
217220
assert wizard.stages['StageTwo'].execute.call_count == 2
218221

219222

@@ -288,6 +291,50 @@ def test_wizard_basic_interaction(wizard_spec):
288291
create.return_value.execute.assert_called_once_with(data)
289292

290293

294+
def test_wizard_basic_delegation(wizard_spec):
295+
main_spec = {
296+
"StartStage": "One",
297+
"Stages": [
298+
{
299+
"Name": "One",
300+
"Prompt": "stage one",
301+
"Retrieval": {
302+
"Type": "Wizard",
303+
"Resource": "SubWizard",
304+
"Path": "FromSub"
305+
}
306+
}
307+
]
308+
}
309+
sub_spec = {
310+
"StartStage": "SubOne",
311+
"Stages": [
312+
{
313+
"Name": "SubOne",
314+
"Prompt": "stage one",
315+
"Retrieval": {
316+
"Type": "Static",
317+
"Resource": {"FromSub": "Result from sub"}
318+
}
319+
}
320+
]
321+
}
322+
323+
mock_loader = mock.Mock(spec=Loader)
324+
mock_loader.list_available_services.return_value = ['wizards']
325+
mock_load_model = mock_loader.load_service_model
326+
mock_load_model.return_value = sub_spec
327+
328+
session = botocore.session.get_session()
329+
session.register_component('data_loader', mock_loader)
330+
loader = WizardLoader(session)
331+
wizard = loader.create_wizard(main_spec)
332+
333+
result = wizard.execute()
334+
mock_load_model.assert_called_once_with('wizards', 'SubWizard')
335+
assert result == 'Result from sub'
336+
337+
291338
exceptions = [
292339
BotoCoreError(),
293340
WizardException('error'),

0 commit comments

Comments
 (0)