Merge lp:~veebers/juju-ci-tools/record_timing_actions into lp:juju-ci-tools

Proposed by Christopher Lee
Status: Superseded
Proposed branch: lp:~veebers/juju-ci-tools/record_timing_actions
Merge into: lp:juju-ci-tools
Diff against target: 2028 lines (+625/-178)
14 files modified
assess_container_networking.py (+3/-2)
chaos.py (+3/-2)
deploy_stack.py (+8/-7)
jujupy/client.py (+192/-31)
jujupy/fake.py (+5/-1)
jujupy/tests/test_client.py (+237/-66)
jujupy/tests/test_version_client.py (+46/-44)
jujupy/version_client.py (+21/-10)
tests/__init__.py (+21/-1)
tests/test_assess_block.py (+2/-1)
tests/test_assess_bootstrap.py (+2/-1)
tests/test_assess_container_networking.py (+28/-2)
tests/test_assess_user_grant_revoke.py (+2/-1)
tests/test_deploy_stack.py (+55/-9)
To merge this branch: bzr merge lp:~veebers/juju-ci-tools/record_timing_actions
Reviewer Review Type Date Requested Status
Juju Release Engineering Pending
Review via email: mp+321134@code.launchpad.net

This proposal has been superseded by a proposal from 2017-03-29.

Commit message

Enable collection of timing data for bootstrap, kill/destroy controller and deploy.

Description of the change

Enable collection of timing data for bootstrap, kill/destroy controller and deploy.

Timing collection for bootstrap and kill/destroy controller are collected without any need for any intervention from a test author.
deploy on the other hand needs to make a change where instead of:
  client.deploy(...)
  client.wait_for_started()
Instead:
  _, deploy_complete = client.deploy(...)
  client.wait_for(deploy_complete)

The resulting timing data looks like: http://paste.ubuntu.com/24265304/

To post a comment you must log in.
1966. By Christopher Lee

Don't actually return non-needed CommandComplete objects.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'assess_container_networking.py'
2--- assess_container_networking.py 2017-02-24 18:28:04 +0000
3+++ assess_container_networking.py 2017-03-28 03:22:14 +0000
4@@ -248,8 +248,9 @@
5
6 d = re.search(r'^default\s+via\s+([\d\.]+)\s+', routes, re.MULTILINE)
7 if d:
8- rc = client.juju('ssh', ('--proxy', target,
9- 'ping -c1 -q ' + d.group(1)), check=False)
10+ rc, _ = client.juju(
11+ 'ssh',
12+ ('--proxy', target, 'ping -c1 -q ' + d.group(1)), check=False)
13 if rc != 0:
14 raise ValueError('%s unable to ping default route' % target)
15 else:
16
17=== modified file 'chaos.py'
18--- chaos.py 2016-06-27 11:41:04 +0000
19+++ chaos.py 2017-03-28 03:22:14 +0000
20@@ -146,8 +146,9 @@
21 check_cmd += '/chaos_monkey.' + self.monkey_ids[unit_name]
22 check_cmd += '/chaos_runner.lock'
23 check_cmd += ' ]'
24- if self.client.juju('run', ('--unit', unit_name, check_cmd),
25- check=False):
26+ retvar, _ = self.client.juju(
27+ 'run', ('--unit', unit_name, check_cmd), check=False)
28+ if retvar != 0:
29 return 'done'
30 return 'running'
31
32
33=== modified file 'deploy_stack.py'
34--- deploy_stack.py 2017-03-01 20:34:01 +0000
35+++ deploy_stack.py 2017-03-28 03:22:14 +0000
36@@ -8,7 +8,6 @@
37 from contextlib import nested
38 except ImportError:
39 from contextlib import ExitStack as nested
40-
41 import glob
42 import logging
43 import os
44@@ -18,7 +17,7 @@
45 import subprocess
46 import sys
47 import time
48-import json
49+import yaml
50 import shutil
51
52 from chaos import background_chaos
53@@ -249,10 +248,11 @@
54
55 def dump_juju_timings(client, log_directory):
56 try:
57- with open(os.path.join(log_directory, 'juju_command_times.json'),
58- 'w') as timing_file:
59- json.dump(client.get_juju_timings(), timing_file, indent=2,
60- sort_keys=True)
61+ report_path = os.path.join(log_directory, 'juju_command_times.yaml')
62+ with open(report_path, 'w') as timing_file:
63+ yaml.safe_dump(
64+ client.get_juju_timings(),
65+ timing_file)
66 timing_file.write('\n')
67 except Exception as e:
68 print_now("Failed to save timings")
69@@ -534,9 +534,10 @@
70
71 def create_initial_model(self, upload_tools, series, boot_kwargs):
72 """Create the initial model by bootstrapping."""
73- self.client.bootstrap(
74+ _, bootstrap_complete = self.client.bootstrap(
75 upload_tools=upload_tools, bootstrap_series=series,
76 **boot_kwargs)
77+ self.client.get_controller_client().wait_for(bootstrap_complete)
78
79 def get_hosts(self):
80 """Provide the controller host."""
81
82=== modified file 'jujupy/client.py'
83--- jujupy/client.py 2017-03-27 15:15:05 +0000
84+++ jujupy/client.py 2017-03-28 03:22:14 +0000
85@@ -1095,7 +1095,7 @@
86 self.feature_flags = feature_flags
87 self.debug = debug
88 self._timeout_path = get_timeout_path()
89- self.juju_timings = {}
90+ self.juju_timings = []
91 self.soft_deadline = soft_deadline
92 self._ignore_soft_deadline = False
93
94@@ -1131,6 +1131,9 @@
95 debug = self.debug
96 result = self.__class__(full_path, version, feature_flags, debug,
97 self.soft_deadline)
98+ # Each clone shares a reference to juju_timings allowing us to collect
99+ # all commands run during a test.
100+ result.juju_timings = self.juju_timings
101 return result
102
103 @property
104@@ -1197,7 +1200,11 @@
105 def juju(self, command, args, used_feature_flags,
106 juju_home, model=None, check=True, timeout=None, extra_env=None,
107 suppress_err=False):
108- """Run a command under juju for the current environment."""
109+ """Run a command under juju for the current environment.
110+
111+ :return: Tuple rval, CommandTime rval being the commands exit code and
112+ a CommandTime object used for storing command timing data.
113+ """
114 args = self.full_args(command, args, model, timeout)
115 log.info(' '.join(args))
116 env = self.shell_environ(used_feature_flags, juju_home)
117@@ -1207,17 +1214,17 @@
118 call_func = subprocess.check_call
119 else:
120 call_func = subprocess.call
121- start_time = time.time()
122 # Mutate os.environ instead of supplying env parameter so Windows can
123 # search env['PATH']
124 stderr = subprocess.PIPE if suppress_err else None
125+ # Keep track of commands and how long the take.
126+ command_time = CommandTime(command, args, env)
127 with scoped_environ(env):
128 log.debug('Running juju with env: {}'.format(env))
129 with self._check_timeouts():
130 rval = call_func(args, stderr=stderr)
131- self.juju_timings.setdefault(args, []).append(
132- (time.time() - start_time))
133- return rval
134+ self.juju_timings.append(command_time)
135+ return rval, command_time
136
137 def expect(self, command, args, used_feature_flags, juju_home, model=None,
138 timeout=None, extra_env=None):
139@@ -1296,6 +1303,22 @@
140 self.timeout = timeout
141 self.already_satisfied = already_satisfied
142
143+ def iter_blocking_state(self, status):
144+ """Identify when the condition required is met.
145+
146+ When the operation is complete yield nothing. Otherwise yields a
147+ tuple ('<item detail>', '<state>')
148+ as to why the action cannot be considered complete yet.
149+
150+ An example for a condition of an application being removed:
151+ yield <application name>, 'still-present'
152+ """
153+ raise NotImplementedError()
154+
155+ def do_raise(self, model_name, status):
156+ """Raise exception for when success condition fails to be achieved."""
157+ raise NotImplementedError()
158+
159
160 class ConditionList(BaseCondition):
161 """A list of conditions that support client.wait_for.
162@@ -1325,6 +1348,15 @@
163 self._conditions[0].do_raise(model_name, status)
164
165
166+class NoopCondition(BaseCondition):
167+
168+ def iter_blocking_state(self, status):
169+ return iter(())
170+
171+ def do_raise(self, model_name, status):
172+ raise Exception('NoopCondition failed: {}'.format(model_name))
173+
174+
175 class WaitMachineNotPresent(BaseCondition):
176 """Condition satisfied when a given machine is not present."""
177
178@@ -1419,6 +1451,103 @@
179 raise VersionsNotUpdated(model_name, status)
180
181
182+class WaitAgentsStarted(BaseCondition):
183+
184+ def __init__(self, timeout=1200):
185+ super(WaitAgentsStarted, self).__init__(timeout)
186+
187+ def iter_blocking_state(self, status):
188+ states = Status.check_agents_started(status)
189+
190+ if states is not None:
191+ for state, item in states.items():
192+ yield item[0], state
193+
194+ def do_raise(self, model_name, status):
195+ raise AgentsNotStarted(model_name, status)
196+
197+
198+class CommandTime:
199+ """Store timing details for a juju command."""
200+
201+ def __init__(self, cmd, full_args, envvars=None, start=None):
202+ """Constructor.
203+
204+ :param cmd: Command string for command run (e.g. bootstrap)
205+ :param args: List of all args the command was called with.
206+ :param envvars: Dict of any extra envvars set before command was
207+ called.
208+ :param start: datetime.datetime object representing when the command
209+ was run. If None defaults to datetime.utcnow()
210+ """
211+ self.cmd = cmd
212+ self.full_args = full_args
213+ self.envvars = envvars
214+ self.start = start if start else datetime.utcnow()
215+ self.end = None
216+
217+ def actual_completion(self, end=None):
218+ """Signify that actual completion time of the command.
219+
220+ Note. ignores multiple calls after the initial call.
221+
222+ :param end: datetime.datetime object. If None defaults to
223+ datetime.datetime.utcnow()
224+ """
225+ if self.end is None:
226+ self.end = end if end else datetime.utcnow()
227+
228+ @property
229+ def total_seconds(self):
230+ """Total amount of seconds a command took to complete.
231+
232+ :return: Int representing number of seconds or None if the command
233+ timing has never been completed.
234+ """
235+ if self.end is None:
236+ return None
237+ return (self.end - self.start).total_seconds()
238+
239+
240+class CommandComplete(BaseCondition):
241+ """Wraps a CommandTime and gives the ability to wait_for completion."""
242+
243+ def __init__(self, real_condition, command_time):
244+ """Constructor.
245+
246+ :param real_condition: BaseCondition object.
247+ :param command_time: CommandTime object representing the command to
248+ wait for completion.
249+ """
250+ super(CommandComplete, self).__init__(
251+ real_condition.timeout,
252+ real_condition.already_satisfied)
253+ self._real_condition = real_condition
254+ self.command_time = command_time
255+ if real_condition.already_satisfied:
256+ self.command_time.actual_completion()
257+
258+ def iter_blocking_state(self, status):
259+ """Wraps the iter_blocking_state of the stored BaseCondition.
260+
261+ When the operation is complete iter_blocking_state yields nothing.
262+ Otherwise iter_blocking_state yields details as to why the action
263+ cannot be considered complete yet.
264+ """
265+ completed = True
266+ for item, state in self._real_condition.iter_blocking_state(status):
267+ completed = False
268+ yield item, state
269+ if completed:
270+ self.command_time.actual_completion()
271+
272+ def do_raise(self, status):
273+ raise RuntimeError(
274+ 'Timed out waiting for "{}" command to complete: "{}"'.format(
275+ self.command_time.cmd,
276+ ' '.join(self.command_time.full_args)))
277+
278+
279 class ModelClient:
280 """Wraps calls to a juju instance, associated with a single model.
281
282@@ -1833,7 +1962,8 @@
283 upload_tools, config_filename, bootstrap_series, credential,
284 auto_upgrade, metadata_source, no_gui, agent_version)
285 self.update_user_name()
286- self.juju('bootstrap', args, include_e=False)
287+ retvar, ct = self.juju('bootstrap', args, include_e=False)
288+ return retvar, CommandComplete(WaitAgentsStarted(), ct)
289
290 @contextmanager
291 def bootstrap_async(self, upload_tools=False, bootstrap_series=None,
292@@ -1863,7 +1993,7 @@
293 '--config', config_file))
294
295 def destroy_model(self):
296- exit_status = self.juju(
297+ exit_status, _ = self.juju(
298 'destroy-model', (self.env.environment, '-y',),
299 include_e=False, timeout=get_teardown_timeout(self))
300 return exit_status
301@@ -1871,22 +2001,32 @@
302 def kill_controller(self, check=False):
303 """Kill a controller and its models. Hard kill option.
304
305- :return: Subprocess's exit code."""
306- return self.juju(
307+ :return: Tuple: Subprocess's exit code, CommandComplete object.
308+ """
309+ retvar, ct = self.juju(
310 'kill-controller', (self.env.controller.name, '-y'),
311 include_e=False, check=check, timeout=get_teardown_timeout(self))
312+ # Already satisfied as this is a sync, operation.
313+ return retvar, CommandComplete(
314+ NoopCondition(already_satisfied=True), ct)
315
316 def destroy_controller(self, all_models=False):
317 """Destroy a controller and its models. Soft kill option.
318
319 :param all_models: If true will attempt to destroy all the
320 controller's models as well.
321- :raises: subprocess.CalledProcessError if the operation fails."""
322+ :raises: subprocess.CalledProcessError if the operation fails.
323+ :return: Tuple: Subprocess's exit code, CommandComplete object.
324+ """
325 args = (self.env.controller.name, '-y')
326 if all_models:
327 args += ('--destroy-all-models',)
328- return self.juju('destroy-controller', args, include_e=False,
329- timeout=get_teardown_timeout(self))
330+ retvar, ct = self.juju(
331+ 'destroy-controller', args, include_e=False,
332+ timeout=get_teardown_timeout(self))
333+ # Already satisfied as this is a sync, operation.
334+ return retvar, CommandComplete(
335+ NoopCondition(already_satisfied=True), ct)
336
337 def tear_down(self):
338 """Tear down the client as cleanly as possible.
339@@ -1897,7 +2037,7 @@
340 self.destroy_controller(all_models=True)
341 except subprocess.CalledProcessError:
342 logging.warning('tear_down destroy-controller failed')
343- retval = self.kill_controller()
344+ retval, _ = self.kill_controller()
345 message = 'tear_down kill-controller result={}'.format(retval)
346 if retval == 0:
347 logging.info(message)
348@@ -1971,7 +2111,8 @@
349
350 def set_model_constraints(self, constraints):
351 constraint_strings = self._dict_as_option_strings(constraints)
352- return self.juju('set-model-constraints', constraint_strings)
353+ retvar, ct = self.juju('set-model-constraints', constraint_strings)
354+ return retvar, CommandComplete(NoopCondition(), ct)
355
356 def get_model_config(self):
357 """Return the value of the environment's configured options."""
358@@ -1986,11 +2127,13 @@
359 def set_env_option(self, option, value):
360 """Set the value of the option in the environment."""
361 option_value = "%s=%s" % (option, value)
362- return self.juju('model-config', (option_value,))
363+ retvar, ct = self.juju('model-config', (option_value,))
364+ return CommandComplete(NoopCondition(), ct)
365
366 def unset_env_option(self, option):
367 """Unset the value of the option in the environment."""
368- return self.juju('model-config', ('--reset', option,))
369+ retvar, ct = self.juju('model-config', ('--reset', option,))
370+ return CommandComplete(NoopCondition(), ct)
371
372 @staticmethod
373 def _format_cloud_region(cloud=None, region=None):
374@@ -2074,13 +2217,22 @@
375
376 def controller_juju(self, command, args):
377 args = ('-c', self.env.controller.name) + args
378- return self.juju(command, args, include_e=False)
379+ retvar, ct = self.juju(command, args, include_e=False)
380+ return CommandComplete(NoopCondition(), ct)
381
382 def get_juju_timings(self):
383- stringified_timings = {}
384- for command, timings in self._backend.juju_timings.items():
385- stringified_timings[' '.join(command)] = timings
386- return stringified_timings
387+ timing_breakdown = []
388+ for ct in self._backend.juju_timings:
389+ timing_breakdown.append(
390+ {
391+ 'command': ct.cmd,
392+ 'full_args': ct.full_args,
393+ 'start': ct.start,
394+ 'end': ct.end,
395+ 'total_seconds': ct.total_seconds,
396+ }
397+ )
398+ return timing_breakdown
399
400 def juju_async(self, command, args, include_e=True, timeout=None):
401 model = self._cmd_model(include_e, controller=False)
402@@ -2111,11 +2263,13 @@
403 args.extend(['--bind', bind])
404 if alias is not None:
405 args.extend([alias])
406- return self.juju('deploy', tuple(args))
407+ retvar, ct = self.juju('deploy', tuple(args))
408+ return retvar, CommandComplete(WaitAgentsStarted(), ct)
409
410 def attach(self, service, resource):
411 args = (service, resource)
412- return self.juju('attach', args)
413+ retvar, ct = self.juju('attach', args)
414+ return retvar, CommandComplete(NoopCondition(), ct)
415
416 def list_resources(self, service_or_unit, details=True):
417 args = ('--format', 'yaml', service_or_unit)
418@@ -2856,11 +3010,14 @@
419 args = ('generate-tools', '-d', source_dir)
420 if stream is not None:
421 args += ('--stream', stream)
422- return self.juju('metadata', args, include_e=False)
423+ retvar, ct = self.juju('metadata', args, include_e=False)
424+ return retvar, CommandComplete(NoopCondition(), ct)
425
426 def add_cloud(self, cloud_name, cloud_file):
427- return self.juju('add-cloud', ("--replace", cloud_name, cloud_file),
428- include_e=False)
429+ retvar, ct = self.juju(
430+ 'add-cloud', ("--replace", cloud_name, cloud_file),
431+ include_e=False)
432+ return retvar, CommandComplete(NoopCondition(), ct)
433
434 def add_cloud_interactive(self, cloud_name, cloud):
435 child = self.expect('add-cloud', include_e=False)
436@@ -2970,11 +3127,13 @@
437
438 def disable_command(self, command_set, message=''):
439 """Disable a command-set."""
440- return self.juju('disable-command', (command_set, message))
441+ retvar, ct = self.juju('disable-command', (command_set, message))
442+ return retvar, CommandComplete(NoopCondition(), ct)
443
444 def enable_command(self, args):
445 """Enable a command-set."""
446- return self.juju('enable-command', args)
447+ retvar, ct = self.juju('enable-command', args)
448+ return CommandComplete(NoopCondition(), ct)
449
450 def sync_tools(self, local_dir=None, stream=None, source=None):
451 """Copy tools into a local directory or model."""
452@@ -2984,10 +3143,12 @@
453 if source is not None:
454 args += ('--source', source)
455 if local_dir is None:
456- return self.juju('sync-tools', args)
457+ retvar, ct = self.juju('sync-tools', args)
458+ return retvar, CommandComplete(NoopCondition(), ct)
459 else:
460 args += ('--local-dir', local_dir)
461- return self.juju('sync-tools', args, include_e=False)
462+ retvar, ct = self.juju('sync-tools', args, include_e=False)
463+ return retvar, CommandComplete(NoopCondition(), ct)
464
465 def switch(self, model=None, controller=None):
466 """Switch between models."""
467
468=== modified file 'jujupy/fake.py'
469--- jujupy/fake.py 2017-03-27 15:15:05 +0000
470+++ jujupy/fake.py 2017-03-28 03:22:14 +0000
471@@ -35,6 +35,7 @@
472 JujuData,
473 SoftDeadlineExceeded,
474 )
475+from jujupy.client import CommandTime
476
477 __metaclass__ = type
478
479@@ -927,6 +928,7 @@
480 num = int(parsed.n or 1)
481 self.deploy(model_state, parsed.charm_name, num,
482 parsed.service_name, parsed.series)
483+ return (0, CommandTime(command, args))
484 if command == 'remove-application':
485 model_state.destroy_service(*args)
486 if command == 'add-relation':
487@@ -975,8 +977,9 @@
488 self.controller_state.destroy()
489 if command == 'kill-controller':
490 if self.controller_state.state == 'not-bootstrapped':
491- return
492+ return (0, CommandTime(command, args))
493 self.controller_state.destroy(kill=True)
494+ return (0, CommandTime(command, args))
495 if command == 'destroy-model':
496 if not self.is_feature_enabled('jes'):
497 raise JESNotSupported()
498@@ -1027,6 +1030,7 @@
499 self.controller_state.shares.remove(username)
500 if command == 'restore-backup':
501 model_state.restore_backup()
502+ return 0, CommandTime(command, args)
503
504 @contextmanager
505 def juju_async(self, command, args, used_feature_flags,
506
507=== modified file 'jujupy/tests/test_client.py'
508--- jujupy/tests/test_client.py 2017-03-24 23:50:18 +0000
509+++ jujupy/tests/test_client.py 2017-03-28 03:22:14 +0000
510@@ -43,6 +43,8 @@
511 AgentUnresolvedError,
512 AppError,
513 BaseCondition,
514+ CommandTime,
515+ CommandComplete,
516 CannotConnectEnv,
517 ConditionList,
518 Controller,
519@@ -68,6 +70,7 @@
520 make_safe_config,
521 ModelClient,
522 NameNotAccepted,
523+ NoopCondition,
524 NoProvider,
525 parse_new_state_server_from_error,
526 ProvisioningError,
527@@ -99,9 +102,11 @@
528 from tests import (
529 assert_juju_call,
530 client_past_deadline,
531+ make_fake_juju_return,
532 FakeHomeTestCase,
533 FakePopen,
534 observable_temp_file,
535+ patch_juju_call,
536 TestCase,
537 )
538 from jujupy.utility import (
539@@ -176,6 +181,12 @@
540 self.assertIsNot(cloned, backend)
541 self.assertIs(soft_deadline, cloned.soft_deadline)
542
543+ def test_cloned_backends_share_juju_timings(self):
544+ backend = Juju2Backend('/bin/path', '2.0', set(), False)
545+ cloned = backend.clone(
546+ full_path=None, version=None, debug=None, feature_flags=None)
547+ self.assertIs(cloned.juju_timings, backend.juju_timings)
548+
549 def test__check_timeouts(self):
550 backend = Juju2Backend('/bin/path', '2.0', set(), debug=False,
551 soft_deadline=datetime(2015, 1, 2, 3, 4, 5))
552@@ -348,6 +359,21 @@
553 list(conditions.iter_blocking_state(None)))
554
555
556+class TestNoopCondition(ClientTest):
557+
558+ def test_iter_blocking_state_is_noop(self):
559+ condition = NoopCondition()
560+ called = False
561+ for _ in condition.iter_blocking_state({}):
562+ called = True
563+ self.assertFalse(called)
564+
565+ def test_do_raise_raises_Exception(self):
566+ condition = NoopCondition()
567+ with self.assertRaises(Exception):
568+ condition.do_raise('model_name', {})
569+
570+
571 class TestWaitMachineNotPresent(ClientTest):
572
573 def test_iter_blocking_state(self):
574@@ -635,7 +661,7 @@
575
576 def test_bootstrap_maas(self):
577 env = JujuData('maas', {'type': 'foo', 'region': 'asdf'})
578- with patch.object(ModelClient, 'juju') as mock:
579+ with patch_juju_call(ModelClient) as mock:
580 client = ModelClient(env, '2.0-zeta1', None)
581 with patch.object(client.env, 'maas', lambda: True):
582 with observable_temp_file() as config_file:
583@@ -653,7 +679,7 @@
584 # Disable space constraint with environment variable
585 os.environ['JUJU_CI_SPACELESSNESS'] = "1"
586 env = JujuData('maas', {'type': 'foo', 'region': 'asdf'})
587- with patch.object(ModelClient, 'juju') as mock:
588+ with patch_juju_call(ModelClient) as mock:
589 client = ModelClient(env, '2.0-zeta1', None)
590 with patch.object(client.env, 'maas', lambda: True):
591 with observable_temp_file() as config_file:
592@@ -669,13 +695,13 @@
593 def test_bootstrap_joyent(self):
594 env = JujuData('joyent', {
595 'type': 'joyent', 'sdc-url': 'https://foo.api.joyentcloud.com'})
596- with patch.object(ModelClient, 'juju', autospec=True) as mock:
597- client = ModelClient(env, '2.0-zeta1', None)
598+ client = ModelClient(env, '2.0-zeta1', None)
599+ with patch_juju_call(client) as mock:
600 with patch.object(client.env, 'joyent', lambda: True):
601 with observable_temp_file() as config_file:
602 client.bootstrap()
603 mock.assert_called_once_with(
604- client, 'bootstrap', (
605+ 'bootstrap', (
606 '--constraints', 'mem=2G cpu-cores=1',
607 'joyent/foo', 'joyent',
608 '--config', config_file.name,
609@@ -685,7 +711,7 @@
610 def test_bootstrap(self):
611 env = JujuData('foo', {'type': 'bar', 'region': 'baz'})
612 with observable_temp_file() as config_file:
613- with patch.object(ModelClient, 'juju') as mock:
614+ with patch_juju_call(ModelClient) as mock:
615 client = ModelClient(env, '2.0-zeta1', None)
616 client.bootstrap()
617 mock.assert_called_with(
618@@ -702,7 +728,7 @@
619 env = JujuData('foo', {'type': 'foo', 'region': 'baz'})
620 client = ModelClient(env, '2.0-zeta1', None)
621 with observable_temp_file() as config_file:
622- with patch.object(client, 'juju') as mock:
623+ with patch_juju_call(client) as mock:
624 client.bootstrap(upload_tools=True)
625 mock.assert_called_with(
626 'bootstrap', (
627@@ -715,7 +741,7 @@
628 env = JujuData('foo', {'type': 'foo', 'region': 'baz'})
629 client = ModelClient(env, '2.0-zeta1', None)
630 with observable_temp_file() as config_file:
631- with patch.object(client, 'juju') as mock:
632+ with patch_juju_call(client) as mock:
633 client.bootstrap(credential='credential_name')
634 mock.assert_called_with(
635 'bootstrap', (
636@@ -728,7 +754,7 @@
637 def test_bootstrap_bootstrap_series(self):
638 env = JujuData('foo', {'type': 'bar', 'region': 'baz'})
639 client = ModelClient(env, '2.0-zeta1', None)
640- with patch.object(client, 'juju') as mock:
641+ with patch_juju_call(client) as mock:
642 with observable_temp_file() as config_file:
643 client.bootstrap(bootstrap_series='angsty')
644 mock.assert_called_with(
645@@ -742,7 +768,7 @@
646 def test_bootstrap_auto_upgrade(self):
647 env = JujuData('foo', {'type': 'bar', 'region': 'baz'})
648 client = ModelClient(env, '2.0-zeta1', None)
649- with patch.object(client, 'juju') as mock:
650+ with patch_juju_call(client) as mock:
651 with observable_temp_file() as config_file:
652 client.bootstrap(auto_upgrade=True)
653 mock.assert_called_with(
654@@ -755,7 +781,7 @@
655 def test_bootstrap_no_gui(self):
656 env = JujuData('foo', {'type': 'bar', 'region': 'baz'})
657 client = ModelClient(env, '2.0-zeta1', None)
658- with patch.object(client, 'juju') as mock:
659+ with patch_juju_call(client) as mock:
660 with observable_temp_file() as config_file:
661 client.bootstrap(no_gui=True)
662 mock.assert_called_with(
663@@ -768,7 +794,7 @@
664 def test_bootstrap_metadata(self):
665 env = JujuData('foo', {'type': 'bar', 'region': 'baz'})
666 client = ModelClient(env, '2.0-zeta1', None)
667- with patch.object(client, 'juju') as mock:
668+ with patch_juju_call(client) as mock:
669 with observable_temp_file() as config_file:
670 client.bootstrap(metadata_source='/var/test-source')
671 mock.assert_called_with(
672@@ -862,7 +888,7 @@
673 client = ModelClient(model_data, None, None)
674 with patch.object(client, 'get_jes_command',
675 return_value=jes_command):
676- with patch.object(controller_client, 'juju') as ccj_mock:
677+ with patch_juju_call(controller_client) as ccj_mock:
678 with observable_temp_file() as config_file:
679 controller_client.add_model(model_data)
680 ccj_mock.assert_called_once_with(
681@@ -874,7 +900,7 @@
682 client.bootstrap()
683 client.env.controller.explicit_region = True
684 model = client.env.clone('new-model')
685- with patch.object(client._backend, 'juju') as juju_mock:
686+ with patch_juju_call(client._backend) as juju_mock:
687 with observable_temp_file() as config_file:
688 client.add_model(model)
689 juju_mock.assert_called_once_with('add-model', (
690@@ -886,7 +912,7 @@
691 def test_add_model_by_name(self):
692 client = fake_juju_client()
693 client.bootstrap()
694- with patch.object(client._backend, 'juju') as juju_mock:
695+ with patch_juju_call(client._backend) as juju_mock:
696 with observable_temp_file() as config_file:
697 client.add_model('new-model')
698 juju_mock.assert_called_once_with('add-model', (
699@@ -902,7 +928,7 @@
700 def test_destroy_model(self):
701 env = JujuData('foo', {'type': 'ec2'})
702 client = ModelClient(env, None, None)
703- with patch.object(client, 'juju') as mock:
704+ with patch_juju_call(client) as mock:
705 client.destroy_model()
706 mock.assert_called_with(
707 'destroy-model', ('foo', '-y'),
708@@ -911,7 +937,7 @@
709 def test_destroy_model_azure(self):
710 env = JujuData('foo', {'type': 'azure'})
711 client = ModelClient(env, None, None)
712- with patch.object(client, 'juju') as mock:
713+ with patch_juju_call(client) as mock:
714 client.destroy_model()
715 mock.assert_called_with(
716 'destroy-model', ('foo', '-y'),
717@@ -920,7 +946,7 @@
718 def test_destroy_model_gce(self):
719 env = JujuData('foo', {'type': 'gce'})
720 client = ModelClient(env, None, None)
721- with patch.object(client, 'juju') as mock:
722+ with patch_juju_call(client) as mock:
723 client.destroy_model()
724 mock.assert_called_with(
725 'destroy-model', ('foo', '-y'),
726@@ -928,7 +954,7 @@
727
728 def test_kill_controller(self):
729 client = ModelClient(JujuData('foo', {'type': 'ec2'}), None, None)
730- with patch.object(client, 'juju') as juju_mock:
731+ with patch_juju_call(client) as juju_mock:
732 client.kill_controller()
733 juju_mock.assert_called_once_with(
734 'kill-controller', ('foo', '-y'), check=False, include_e=False,
735@@ -936,7 +962,7 @@
736
737 def test_kill_controller_check(self):
738 client = ModelClient(JujuData('foo', {'type': 'ec2'}), None, None)
739- with patch.object(client, 'juju') as juju_mock:
740+ with patch_juju_call(client) as juju_mock:
741 client.kill_controller(check=True)
742 juju_mock.assert_called_once_with(
743 'kill-controller', ('foo', '-y'), check=True, include_e=False,
744@@ -946,7 +972,7 @@
745 client = ModelClient(JujuData('foo', {'type': 'azure'}), None, None)
746 with patch.object(client, 'get_jes_command',
747 return_value=jes_command):
748- with patch.object(client, 'juju') as juju_mock:
749+ with patch_juju_call(client) as juju_mock:
750 client.kill_controller()
751 juju_mock.assert_called_once_with(
752 kill_command, ('foo', '-y'), check=False, include_e=False,
753@@ -954,7 +980,7 @@
754
755 def test_kill_controller_gce(self):
756 client = ModelClient(JujuData('foo', {'type': 'gce'}), None, None)
757- with patch.object(client, 'juju') as juju_mock:
758+ with patch_juju_call(client) as juju_mock:
759 client.kill_controller()
760 juju_mock.assert_called_once_with(
761 'kill-controller', ('foo', '-y'), check=False, include_e=False,
762@@ -962,7 +988,7 @@
763
764 def test_destroy_controller(self):
765 client = ModelClient(JujuData('foo', {'type': 'ec2'}), None, None)
766- with patch.object(client, 'juju') as juju_mock:
767+ with patch_juju_call(client) as juju_mock:
768 client.destroy_controller()
769 juju_mock.assert_called_once_with(
770 'destroy-controller', ('foo', '-y'), include_e=False,
771@@ -970,7 +996,7 @@
772
773 def test_destroy_controller_all_models(self):
774 client = ModelClient(JujuData('foo', {'type': 'ec2'}), None, None)
775- with patch.object(client, 'juju') as juju_mock:
776+ with patch_juju_call(client) as juju_mock:
777 client.destroy_controller(all_models=True)
778 juju_mock.assert_called_once_with(
779 'destroy-controller', ('foo', '-y', '--destroy-all-models'),
780@@ -988,7 +1014,9 @@
781 side_effect=raise_error) as mock:
782 yield mock
783 else:
784- with patch.object(target, attribute, autospec=True) as mock:
785+ with patch.object(
786+ target, attribute, autospec=True,
787+ return_value=make_fake_juju_return()) as mock:
788 yield mock
789
790 with patch_raise(client, 'destroy_controller', destroy_raises
791@@ -1219,21 +1247,21 @@
792 def test_deploy_non_joyent(self):
793 env = ModelClient(
794 JujuData('foo', {'type': 'local'}), '1.234-76', None)
795- with patch.object(env, 'juju') as mock_juju:
796+ with patch_juju_call(env) as mock_juju:
797 env.deploy('mondogb')
798 mock_juju.assert_called_with('deploy', ('mondogb',))
799
800 def test_deploy_joyent(self):
801 env = ModelClient(
802 JujuData('foo', {'type': 'local'}), '1.234-76', None)
803- with patch.object(env, 'juju') as mock_juju:
804+ with patch_juju_call(env) as mock_juju:
805 env.deploy('mondogb')
806 mock_juju.assert_called_with('deploy', ('mondogb',))
807
808 def test_deploy_repository(self):
809 env = ModelClient(
810 JujuData('foo', {'type': 'local'}), '1.234-76', None)
811- with patch.object(env, 'juju') as mock_juju:
812+ with patch_juju_call(env) as mock_juju:
813 env.deploy('/home/jrandom/repo/mongodb')
814 mock_juju.assert_called_with(
815 'deploy', ('/home/jrandom/repo/mongodb',))
816@@ -1241,7 +1269,7 @@
817 def test_deploy_to(self):
818 env = ModelClient(
819 JujuData('foo', {'type': 'local'}), '1.234-76', None)
820- with patch.object(env, 'juju') as mock_juju:
821+ with patch_juju_call(env) as mock_juju:
822 env.deploy('mondogb', to='0')
823 mock_juju.assert_called_with(
824 'deploy', ('mondogb', '--to', '0'))
825@@ -1249,7 +1277,7 @@
826 def test_deploy_service(self):
827 env = ModelClient(
828 JujuData('foo', {'type': 'local'}), '1.234-76', None)
829- with patch.object(env, 'juju') as mock_juju:
830+ with patch_juju_call(env) as mock_juju:
831 env.deploy('local:mondogb', service='my-mondogb')
832 mock_juju.assert_called_with(
833 'deploy', ('local:mondogb', 'my-mondogb',))
834@@ -1257,14 +1285,14 @@
835 def test_deploy_force(self):
836 env = ModelClient(
837 JujuData('foo', {'type': 'local'}), '1.234-76', None)
838- with patch.object(env, 'juju') as mock_juju:
839+ with patch_juju_call(env) as mock_juju:
840 env.deploy('local:mondogb', force=True)
841 mock_juju.assert_called_with('deploy', ('local:mondogb', '--force',))
842
843 def test_deploy_series(self):
844 env = ModelClient(
845 JujuData('foo', {'type': 'local'}), '1.234-76', None)
846- with patch.object(env, 'juju') as mock_juju:
847+ with patch_juju_call(env) as mock_juju:
848 env.deploy('local:blah', series='xenial')
849 mock_juju.assert_called_with(
850 'deploy', ('local:blah', '--series', 'xenial'))
851@@ -1272,14 +1300,14 @@
852 def test_deploy_multiple(self):
853 env = ModelClient(
854 JujuData('foo', {'type': 'local'}), '1.234-76', None)
855- with patch.object(env, 'juju') as mock_juju:
856+ with patch_juju_call(env) as mock_juju:
857 env.deploy('local:blah', num=2)
858 mock_juju.assert_called_with(
859 'deploy', ('local:blah', '-n', '2'))
860
861 def test_deploy_resource(self):
862 env = ModelClient(JujuData('foo', {'type': 'local'}), None, None)
863- with patch.object(env, 'juju') as mock_juju:
864+ with patch_juju_call(env) as mock_juju:
865 env.deploy('local:blah', resource='foo=/path/dir')
866 mock_juju.assert_called_with(
867 'deploy', ('local:blah', '--resource', 'foo=/path/dir'))
868@@ -1287,7 +1315,7 @@
869 def test_deploy_storage(self):
870 env = EnvJujuClient1X(
871 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
872- with patch.object(env, 'juju') as mock_juju:
873+ with patch_juju_call(env) as mock_juju:
874 env.deploy('mondogb', storage='rootfs,1G')
875 mock_juju.assert_called_with(
876 'deploy', ('mondogb', '--storage', 'rootfs,1G'))
877@@ -1295,27 +1323,27 @@
878 def test_deploy_constraints(self):
879 env = EnvJujuClient1X(
880 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
881- with patch.object(env, 'juju') as mock_juju:
882+ with patch_juju_call(env) as mock_juju:
883 env.deploy('mondogb', constraints='virt-type=kvm')
884 mock_juju.assert_called_with(
885 'deploy', ('mondogb', '--constraints', 'virt-type=kvm'))
886
887 def test_deploy_bind(self):
888 env = ModelClient(JujuData('foo', {'type': 'local'}), None, None)
889- with patch.object(env, 'juju') as mock_juju:
890+ with patch_juju_call(env) as mock_juju:
891 env.deploy('mydb', bind='backspace')
892 mock_juju.assert_called_with('deploy', ('mydb', '--bind', 'backspace'))
893
894 def test_deploy_aliased(self):
895 env = ModelClient(JujuData('foo', {'type': 'local'}), None, None)
896- with patch.object(env, 'juju') as mock_juju:
897+ with patch_juju_call(env) as mock_juju:
898 env.deploy('local:blah', alias='blah-blah')
899 mock_juju.assert_called_with(
900 'deploy', ('local:blah', 'blah-blah'))
901
902 def test_attach(self):
903 env = ModelClient(JujuData('foo', {'type': 'local'}), None, None)
904- with patch.object(env, 'juju') as mock_juju:
905+ with patch_juju_call(env) as mock_juju:
906 env.attach('foo', resource='foo=/path/dir')
907 mock_juju.assert_called_with('attach', ('foo', 'foo=/path/dir'))
908
909@@ -1383,7 +1411,7 @@
910 def test_deploy_bundle_2x(self):
911 client = ModelClient(JujuData('an_env', None),
912 '1.23-series-arch', None)
913- with patch.object(client, 'juju') as mock_juju:
914+ with patch_juju_call(client) as mock_juju:
915 client.deploy_bundle('bundle:~juju-qa/some-bundle')
916 mock_juju.assert_called_with(
917 'deploy', ('bundle:~juju-qa/some-bundle'), timeout=3600)
918@@ -1391,7 +1419,7 @@
919 def test_deploy_bundle_template(self):
920 client = ModelClient(JujuData('an_env', None),
921 '1.23-series-arch', None)
922- with patch.object(client, 'juju') as mock_juju:
923+ with patch_juju_call(client) as mock_juju:
924 client.deploy_bundle('bundle:~juju-qa/some-{container}-bundle')
925 mock_juju.assert_called_with(
926 'deploy', ('bundle:~juju-qa/some-lxd-bundle'), timeout=3600)
927@@ -1399,7 +1427,7 @@
928 def test_upgrade_charm(self):
929 env = ModelClient(
930 JujuData('foo', {'type': 'local'}), '2.34-74', None)
931- with patch.object(env, 'juju') as mock_juju:
932+ with patch_juju_call(env) as mock_juju:
933 env.upgrade_charm('foo-service',
934 '/bar/repository/angsty/mongodb')
935 mock_juju.assert_called_once_with(
936@@ -1409,7 +1437,7 @@
937 def test_remove_service(self):
938 env = ModelClient(
939 JujuData('foo', {'type': 'local'}), '1.234-76', None)
940- with patch.object(env, 'juju') as mock_juju:
941+ with patch_juju_call(env) as mock_juju:
942 env.remove_service('mondogb')
943 mock_juju.assert_called_with('remove-application', ('mondogb',))
944
945@@ -1578,7 +1606,7 @@
946
947 def test_remove_machine(self):
948 client = fake_juju_client()
949- with patch.object(client._backend, 'juju') as juju_mock:
950+ with patch_juju_call(client._backend) as juju_mock:
951 condition = client.remove_machine('0')
952 call = backend_call(
953 client, 'remove-machine', ('0',), 'name:name')
954@@ -1587,7 +1615,7 @@
955
956 def test_remove_machine_force(self):
957 client = fake_juju_client()
958- with patch.object(client._backend, 'juju') as juju_mock:
959+ with patch_juju_call(client._backend) as juju_mock:
960 client.remove_machine('0', force=True)
961 call = backend_call(
962 client, 'remove-machine', ('--force', '0'), 'name:name')
963@@ -1982,7 +2010,7 @@
964
965 def test_list_models(self):
966 client = ModelClient(JujuData('foo'), None, None)
967- with patch.object(client, 'juju') as j_mock:
968+ with patch_juju_call(client) as j_mock:
969 client.list_models()
970 j_mock.assert_called_once_with(
971 'list-models', ('-c', 'foo'), include_e=False)
972@@ -2194,7 +2222,7 @@
973
974 def test_list_controllers(self):
975 client = ModelClient(JujuData('foo'), None, None)
976- with patch.object(client, 'juju') as j_mock:
977+ with patch_juju_call(client) as j_mock:
978 client.list_controllers()
979 j_mock.assert_called_once_with('list-controllers', (), include_e=False)
980
981@@ -2662,7 +2690,7 @@
982
983 def test_set_model_constraints(self):
984 client = ModelClient(JujuData('bar', {}), None, '/foo')
985- with patch.object(client, 'juju') as juju_mock:
986+ with patch_juju_call(client) as juju_mock:
987 client.set_model_constraints({'bar': 'baz'})
988 juju_mock.assert_called_once_with('set-model-constraints',
989 ('bar=baz',))
990@@ -2948,7 +2976,7 @@
991 def test_restore_backup(self):
992 env = JujuData('qux')
993 client = ModelClient(env, None, '/foobar/baz')
994- with patch.object(client, 'juju') as gjo_mock:
995+ with patch_juju_call(client) as gjo_mock:
996 client.restore_backup('quxx')
997 gjo_mock.assert_called_once_with(
998 'restore-backup',
999@@ -3038,12 +3066,33 @@
1000 self.assertEqual(0, po_mock.call_count)
1001
1002 def test_get_juju_timings(self):
1003+ first_start = datetime(2017, 3, 22, 23, 36, 52, 0)
1004+ first_end = first_start + timedelta(seconds=2)
1005+ second_start = datetime(2017, 5, 22, 23, 36, 52, 0)
1006 env = JujuData('foo')
1007 client = ModelClient(env, None, 'my/juju/bin')
1008- client._backend.juju_timings = {("juju", "op1"): [1],
1009- ("juju", "op2"): [2]}
1010+ client._backend.juju_timings.extend([
1011+ CommandTime('command1', ['command1', 'arg1'], start=first_start),
1012+ CommandTime(
1013+ 'command2', ['command2', 'arg1', 'arg2'], start=second_start)])
1014+ client._backend.juju_timings[0].actual_completion(end=first_end)
1015 flattened_timings = client.get_juju_timings()
1016- expected = {"juju op1": [1], "juju op2": [2]}
1017+ expected = [
1018+ {
1019+ 'command': 'command1',
1020+ 'full_args': ['command1', 'arg1'],
1021+ 'start': first_start,
1022+ 'end': first_end,
1023+ 'total_seconds': 2,
1024+ },
1025+ {
1026+ 'command': 'command2',
1027+ 'full_args': ['command2', 'arg1', 'arg2'],
1028+ 'start': second_start,
1029+ 'end': None,
1030+ 'total_seconds': None,
1031+ }
1032+ ]
1033 self.assertEqual(flattened_timings, expected)
1034
1035 def test_deployer(self):
1036@@ -3230,7 +3279,7 @@
1037
1038 def test_set_config(self):
1039 client = ModelClient(JujuData('bar', {}), None, '/foo')
1040- with patch.object(client, 'juju') as juju_mock:
1041+ with patch_juju_call(client) as juju_mock:
1042 client.set_config('foo', {'bar': 'baz'})
1043 juju_mock.assert_called_once_with('config', ('foo', 'bar=baz'))
1044
1045@@ -3285,7 +3334,7 @@
1046
1047 def test_upgrade_mongo(self):
1048 client = ModelClient(JujuData('bar', {}), None, '/foo')
1049- with patch.object(client, 'juju') as juju_mock:
1050+ with patch_juju_call(client) as juju_mock:
1051 client.upgrade_mongo()
1052 juju_mock.assert_called_once_with('upgrade-mongo', ())
1053
1054@@ -3330,7 +3379,7 @@
1055 default_model = fake_client.model_name
1056 default_controller = fake_client.env.controller.name
1057
1058- with patch.object(fake_client, 'juju', return_value=True):
1059+ with patch_juju_call(fake_client):
1060 fake_client.revoke(username)
1061 fake_client.juju.assert_called_with('revoke',
1062 ('-c', default_controller,
1063@@ -3410,7 +3459,7 @@
1064 env = JujuData('foo')
1065 username = 'fakeuser'
1066 client = ModelClient(env, None, None)
1067- with patch.object(client, 'juju') as mock:
1068+ with patch_juju_call(client) as mock:
1069 client.disable_user(username)
1070 mock.assert_called_with(
1071 'disable-user', ('-c', 'foo', 'fakeuser'), include_e=False)
1072@@ -3419,7 +3468,7 @@
1073 env = JujuData('foo')
1074 username = 'fakeuser'
1075 client = ModelClient(env, None, None)
1076- with patch.object(client, 'juju') as mock:
1077+ with patch_juju_call(client) as mock:
1078 client.enable_user(username)
1079 mock.assert_called_with(
1080 'enable-user', ('-c', 'foo', 'fakeuser'), include_e=False)
1081@@ -3427,7 +3476,7 @@
1082 def test_logout(self):
1083 env = JujuData('foo')
1084 client = ModelClient(env, None, None)
1085- with patch.object(client, 'juju') as mock:
1086+ with patch_juju_call(client) as mock:
1087 client.logout()
1088 mock.assert_called_with(
1089 'logout', ('-c', 'foo'), include_e=False)
1090@@ -3678,32 +3727,32 @@
1091
1092 def test_disable_command(self):
1093 client = ModelClient(JujuData('foo'), None, None)
1094- with patch.object(client, 'juju', autospec=True) as mock:
1095+ with patch_juju_call(client) as mock:
1096 client.disable_command('all', 'message')
1097 mock.assert_called_once_with('disable-command', ('all', 'message'))
1098
1099 def test_enable_command(self):
1100 client = ModelClient(JujuData('foo'), None, None)
1101- with patch.object(client, 'juju', autospec=True) as mock:
1102+ with patch_juju_call(client) as mock:
1103 client.enable_command('all')
1104 mock.assert_called_once_with('enable-command', 'all')
1105
1106 def test_sync_tools(self):
1107 client = ModelClient(JujuData('foo'), None, None)
1108- with patch.object(client, 'juju', autospec=True) as mock:
1109+ with patch_juju_call(client) as mock:
1110 client.sync_tools()
1111 mock.assert_called_once_with('sync-tools', ())
1112
1113 def test_sync_tools_local_dir(self):
1114 client = ModelClient(JujuData('foo'), None, None)
1115- with patch.object(client, 'juju', autospec=True) as mock:
1116+ with patch_juju_call(client) as mock:
1117 client.sync_tools('/agents')
1118 mock.assert_called_once_with('sync-tools', ('--local-dir', '/agents'),
1119 include_e=False)
1120
1121 def test_generate_tool(self):
1122 client = ModelClient(JujuData('foo'), None, None)
1123- with patch.object(client, 'juju', autospec=True) as mock:
1124+ with patch_juju_call(client) as mock:
1125 client.generate_tool('/agents')
1126 mock.assert_called_once_with('metadata',
1127 ('generate-tools', '-d', '/agents'),
1128@@ -3711,7 +3760,7 @@
1129
1130 def test_generate_tool_with_stream(self):
1131 client = ModelClient(JujuData('foo'), None, None)
1132- with patch.object(client, 'juju', autospec=True) as mock:
1133+ with patch_juju_call(client) as mock:
1134 client.generate_tool('/agents', "testing")
1135 mock.assert_called_once_with(
1136 'metadata', ('generate-tools', '-d', '/agents',
1137@@ -3719,7 +3768,7 @@
1138
1139 def test_add_cloud(self):
1140 client = ModelClient(JujuData('foo'), None, None)
1141- with patch.object(client, 'juju', autospec=True) as mock:
1142+ with patch_juju_call(client) as mock:
1143 client.add_cloud('localhost', 'cfile')
1144 mock.assert_called_once_with('add-cloud',
1145 ('--replace', 'localhost', 'cfile'),
1146@@ -3728,7 +3777,7 @@
1147 def test_switch(self):
1148 def run_switch_test(expect, model=None, controller=None):
1149 client = ModelClient(JujuData('foo'), None, None)
1150- with patch.object(client, 'juju', autospec=True) as mock:
1151+ with patch_juju_call(client) as mock:
1152 client.switch(model=model, controller=controller)
1153 mock.assert_called_once_with('switch', (expect,), include_e=False)
1154 run_switch_test('default', 'default')
1155@@ -6021,3 +6070,125 @@
1156 self.assertEqual(host, "2001:db8::3")
1157 fake_client.status_until.assert_called_once_with(timeout=600)
1158 self.assertEqual(self.log_stream.getvalue(), "")
1159+
1160+
1161+class TestCommandTime(TestCase):
1162+
1163+ def test_default_values(self):
1164+ full_args = ['juju', '--showlog', 'bootstrap']
1165+ utcnow = datetime(2017, 3, 22, 23, 36, 52, 530631)
1166+ with patch('jujupy.client.datetime', autospec=True) as m_dt:
1167+ m_dt.utcnow.return_value = utcnow
1168+ ct = CommandTime('bootstrap', full_args)
1169+ self.assertEqual(ct.cmd, 'bootstrap')
1170+ self.assertEqual(ct.full_args, full_args)
1171+ self.assertEqual(ct.envvars, None)
1172+ self.assertEqual(ct.start, utcnow)
1173+ self.assertEqual(ct.end, None)
1174+
1175+ def test_set_start_time(self):
1176+ ct = CommandTime('cmd', [], start='abc')
1177+ self.assertEqual(ct.start, 'abc')
1178+
1179+ def test_set_envvar(self):
1180+ details = {'abc': 123}
1181+ ct = CommandTime('cmd', [], envvars=details)
1182+ self.assertEqual(ct.envvars, details)
1183+
1184+ def test_actual_completion_sets_default(self):
1185+ utcnow = datetime(2017, 3, 22, 23, 36, 52, 530631)
1186+ ct = CommandTime('cmd', [])
1187+ with patch('jujupy.client.datetime', autospec=True) as m_dt:
1188+ m_dt.utcnow.return_value = utcnow
1189+ ct.actual_completion()
1190+ self.assertEqual(ct.end, utcnow)
1191+
1192+ def test_actual_completion_idempotent(self):
1193+ ct = CommandTime('cmd', [])
1194+ ct.actual_completion(end='a')
1195+ ct.actual_completion(end='b')
1196+ self.assertEqual(ct.end, 'a')
1197+
1198+ def test_actual_completion_set_value(self):
1199+ utcnow = datetime(2017, 3, 22, 23, 36, 52, 530631)
1200+ ct = CommandTime('cmd', [])
1201+ ct.actual_completion(end=utcnow)
1202+ self.assertEqual(ct.end, utcnow)
1203+
1204+ def test_total_seconds_returns_None_when_not_complete(self):
1205+ ct = CommandTime('cmd', [])
1206+ self.assertEqual(ct.total_seconds, None)
1207+
1208+ def test_total_seconds_returns_seconds_taken_to_complete(self):
1209+ utcstart = datetime(2017, 3, 22, 23, 36, 52, 530631)
1210+ utcend = utcstart + timedelta(seconds=1)
1211+ with patch('jujupy.client.datetime', autospec=True) as m_dt:
1212+ m_dt.utcnow.side_effect = [utcstart, utcend]
1213+ ct = CommandTime('cmd', [])
1214+ ct.actual_completion()
1215+ self.assertEqual(ct.total_seconds, 1)
1216+
1217+
1218+class TestCommandComplete(TestCase):
1219+
1220+ def test_default_values(self):
1221+ ct = CommandTime('cmd', [])
1222+ base_condition = BaseCondition()
1223+ cc = CommandComplete(base_condition, ct)
1224+
1225+ self.assertEqual(cc.timeout, 300)
1226+ self.assertEqual(cc.already_satisfied, False)
1227+ self.assertEqual(cc._real_condition, base_condition)
1228+ self.assertEqual(cc.command_time, ct)
1229+ # actual_completion shouldn't be set as the condition is not already
1230+ # satisfied.
1231+ self.assertEqual(cc.command_time.end, None)
1232+
1233+ def test_sets_total_seconds_when_already_satisfied(self):
1234+ base_condition = BaseCondition(already_satisfied=True)
1235+ ct = CommandTime('cmd', [])
1236+ cc = CommandComplete(base_condition, ct)
1237+
1238+ self.assertIsNotNone(cc.command_time.total_seconds)
1239+
1240+ def test_calls_wrapper_condition_iter(self):
1241+ class TestCondition(BaseCondition):
1242+ def iter_blocking_state(self, status):
1243+ yield 'item', status
1244+
1245+ ct = CommandTime('cmd', [])
1246+ cc = CommandComplete(TestCondition(), ct)
1247+
1248+ k, v = next(cc.iter_blocking_state('status_obj'))
1249+ self.assertEqual(k, 'item')
1250+ self.assertEqual(v, 'status_obj')
1251+
1252+ def test_sets_actual_completion_when_complete(self):
1253+ """When the condition hits success must set actual_completion."""
1254+ class TestCondition(BaseCondition):
1255+ def __init__(self):
1256+ super(TestCondition, self).__init__()
1257+ self._already_called = False
1258+
1259+ def iter_blocking_state(self, status):
1260+ if not self._already_called:
1261+ self._already_called = True
1262+ yield 'item', status
1263+
1264+ ct = CommandTime('cmd', [])
1265+ cc = CommandComplete(TestCondition(), ct)
1266+
1267+ next(cc.iter_blocking_state('status_obj'))
1268+ self.assertIsNone(cc.command_time.end)
1269+ next(cc.iter_blocking_state('status_obj'), None)
1270+ self.assertIsNotNone(cc.command_time.end)
1271+
1272+ def test_raises_exception_with_command_details(self):
1273+ ct = CommandTime('cmd', ['cmd', 'arg1', 'arg2'])
1274+ cc = CommandComplete(BaseCondition(), ct)
1275+
1276+ with self.assertRaises(RuntimeError) as ex:
1277+ cc.do_raise('status')
1278+ self.assertEqual(
1279+ str(ex.exception),
1280+ 'Timed out waiting for "cmd" command to complete: "cmd arg1 arg2"')
1281
1282=== modified file 'jujupy/tests/test_version_client.py'
1283--- jujupy/tests/test_version_client.py 2017-03-24 23:50:18 +0000
1284+++ jujupy/tests/test_version_client.py 2017-03-28 03:22:14 +0000
1285@@ -69,6 +69,7 @@
1286 )
1287 from tests import (
1288 assert_juju_call,
1289+ patch_juju_call,
1290 FakeHomeTestCase,
1291 FakePopen,
1292 observable_temp_file,
1293@@ -367,7 +368,7 @@
1294 def test_bootstrap(self):
1295 env = JujuData('foo', {'type': 'bar', 'region': 'baz'})
1296 with observable_temp_file() as config_file:
1297- with patch.object(ModelClientRC, 'juju') as mock:
1298+ with patch_juju_call(ModelClientRC) as mock:
1299 client = ModelClientRC(env, '2.0-zeta1', None)
1300 client.bootstrap()
1301 mock.assert_called_with(
1302@@ -432,7 +433,7 @@
1303 def test_upgrade_juju_nonlocal(self):
1304 client = EnvJujuClient1X(
1305 SimpleEnvironment('foo', {'type': 'nonlocal'}), '1.234-76', None)
1306- with patch.object(client, 'juju') as juju_mock:
1307+ with patch_juju_call(client) as juju_mock:
1308 client.upgrade_juju()
1309 juju_mock.assert_called_with(
1310 'upgrade-juju', ('--version', '1.234'))
1311@@ -440,7 +441,7 @@
1312 def test_upgrade_juju_local(self):
1313 client = EnvJujuClient1X(
1314 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1315- with patch.object(client, 'juju') as juju_mock:
1316+ with patch_juju_call(client) as juju_mock:
1317 client.upgrade_juju()
1318 juju_mock.assert_called_with(
1319 'upgrade-juju', ('--version', '1.234', '--upload-tools',))
1320@@ -448,7 +449,7 @@
1321 def test_upgrade_juju_no_force_version(self):
1322 client = EnvJujuClient1X(
1323 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1324- with patch.object(client, 'juju') as juju_mock:
1325+ with patch_juju_call(client) as juju_mock:
1326 client.upgrade_juju(force_version=False)
1327 juju_mock.assert_called_with(
1328 'upgrade-juju', ('--upload-tools',))
1329@@ -467,7 +468,7 @@
1330
1331 def test_bootstrap_maas(self):
1332 env = SimpleEnvironment('maas')
1333- with patch.object(EnvJujuClient1X, 'juju') as mock:
1334+ with patch_juju_call(EnvJujuClient1X) as mock:
1335 client = EnvJujuClient1X(env, None, None)
1336 with patch.object(client.env, 'maas', lambda: True):
1337 client.bootstrap()
1338@@ -475,24 +476,24 @@
1339
1340 def test_bootstrap_joyent(self):
1341 env = SimpleEnvironment('joyent')
1342- with patch.object(EnvJujuClient1X, 'juju', autospec=True) as mock:
1343+ with patch_juju_call(EnvJujuClient1X) as mock:
1344 client = EnvJujuClient1X(env, None, None)
1345 with patch.object(client.env, 'joyent', lambda: True):
1346 client.bootstrap()
1347 mock.assert_called_once_with(
1348- client, 'bootstrap', ('--constraints', 'mem=2G cpu-cores=1'))
1349+ 'bootstrap', ('--constraints', 'mem=2G cpu-cores=1'))
1350
1351 def test_bootstrap(self):
1352 env = SimpleEnvironment('foo')
1353 client = EnvJujuClient1X(env, None, None)
1354- with patch.object(client, 'juju') as mock:
1355+ with patch_juju_call(client) as mock:
1356 client.bootstrap()
1357 mock.assert_called_with('bootstrap', ('--constraints', 'mem=2G'))
1358
1359 def test_bootstrap_upload_tools(self):
1360 env = SimpleEnvironment('foo')
1361 client = EnvJujuClient1X(env, None, None)
1362- with patch.object(client, 'juju') as mock:
1363+ with patch_juju_call(client) as mock:
1364 client.bootstrap(upload_tools=True)
1365 mock.assert_called_with(
1366 'bootstrap', ('--upload-tools', '--constraints', 'mem=2G'))
1367@@ -508,7 +509,7 @@
1368 env.update_config({
1369 'default-series': 'angsty',
1370 })
1371- with patch.object(client, 'juju') as mock:
1372+ with patch_juju_call(client) as mock:
1373 client.bootstrap(bootstrap_series='angsty')
1374 mock.assert_called_with('bootstrap', ('--constraints', 'mem=2G'))
1375
1376@@ -573,7 +574,8 @@
1377 def test_destroy_environment(self):
1378 env = SimpleEnvironment('foo', {'type': 'ec2'})
1379 client = EnvJujuClient1X(env, None, None)
1380- with patch.object(client, 'juju') as mock:
1381+ with patch.object(
1382+ client, 'juju', autospec=True, return_value=(0, None)) as mock:
1383 client.destroy_environment()
1384 mock.assert_called_with(
1385 'destroy-environment', ('foo', '--force', '-y'),
1386@@ -582,7 +584,9 @@
1387 def test_destroy_environment_no_force(self):
1388 env = SimpleEnvironment('foo', {'type': 'ec2'})
1389 client = EnvJujuClient1X(env, None, None)
1390- with patch.object(client, 'juju') as mock:
1391+ with patch.object(
1392+ client, 'juju',
1393+ autospec=True, return_value=(0, None)) as mock:
1394 client.destroy_environment(force=False)
1395 mock.assert_called_with(
1396 'destroy-environment', ('foo', '-y'),
1397@@ -591,7 +595,8 @@
1398 def test_destroy_environment_azure(self):
1399 env = SimpleEnvironment('foo', {'type': 'azure'})
1400 client = EnvJujuClient1X(env, None, None)
1401- with patch.object(client, 'juju') as mock:
1402+ with patch.object(
1403+ client, 'juju', autospec=True, return_value=(0, None)) as mock:
1404 client.destroy_environment(force=False)
1405 mock.assert_called_with(
1406 'destroy-environment', ('foo', '-y'),
1407@@ -600,7 +605,9 @@
1408 def test_destroy_environment_gce(self):
1409 env = SimpleEnvironment('foo', {'type': 'gce'})
1410 client = EnvJujuClient1X(env, None, None)
1411- with patch.object(client, 'juju') as mock:
1412+ with patch.object(
1413+ client, 'juju',
1414+ autospec=True, return_value=(0, None)) as mock:
1415 client.destroy_environment(force=False)
1416 mock.assert_called_with(
1417 'destroy-environment', ('foo', '-y'),
1418@@ -609,7 +616,8 @@
1419 def test_destroy_environment_delete_jenv(self):
1420 env = SimpleEnvironment('foo', {'type': 'ec2'})
1421 client = EnvJujuClient1X(env, None, None)
1422- with patch.object(client, 'juju'):
1423+ with patch.object(
1424+ client, 'juju', autospec=True, return_value=(0, None)):
1425 with temp_env({}) as juju_home:
1426 client.env.juju_home = juju_home
1427 jenv_path = get_jenv_path(juju_home, 'foo')
1428@@ -622,7 +630,8 @@
1429 def test_destroy_model(self):
1430 env = SimpleEnvironment('foo', {'type': 'ec2'})
1431 client = EnvJujuClient1X(env, None, None)
1432- with patch.object(client, 'juju') as mock:
1433+ with patch.object(
1434+ client, 'juju', autospec=True, return_value=(0, None)) as mock:
1435 client.destroy_model()
1436 mock.assert_called_with(
1437 'destroy-environment', ('foo', '-y'),
1438@@ -631,7 +640,9 @@
1439 def test_kill_controller(self):
1440 client = EnvJujuClient1X(
1441 SimpleEnvironment('foo', {'type': 'ec2'}), None, None)
1442- with patch.object(client, 'juju') as juju_mock:
1443+ with patch.object(
1444+ client, 'juju',
1445+ autospec=True, return_value=(0, None)) as juju_mock:
1446 client.kill_controller()
1447 juju_mock.assert_called_once_with(
1448 'destroy-environment', ('foo', '--force', '-y'), check=False,
1449@@ -640,7 +651,7 @@
1450 def test_kill_controller_check(self):
1451 client = EnvJujuClient1X(
1452 SimpleEnvironment('foo', {'type': 'ec2'}), None, None)
1453- with patch.object(client, 'juju') as juju_mock:
1454+ with patch_juju_call(client) as juju_mock:
1455 client.kill_controller(check=True)
1456 juju_mock.assert_called_once_with(
1457 'destroy-environment', ('foo', '--force', '-y'), check=True,
1458@@ -649,7 +660,7 @@
1459 def test_destroy_controller(self):
1460 client = EnvJujuClient1X(
1461 SimpleEnvironment('foo', {'type': 'ec2'}), None, None)
1462- with patch.object(client, 'juju') as juju_mock:
1463+ with patch_juju_call(client) as juju_mock:
1464 client.destroy_controller()
1465 juju_mock.assert_called_once_with(
1466 'destroy-environment', ('foo', '-y'),
1467@@ -813,21 +824,21 @@
1468 def test_deploy_non_joyent(self):
1469 env = EnvJujuClient1X(
1470 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1471- with patch.object(env, 'juju') as mock_juju:
1472+ with patch_juju_call(env) as mock_juju:
1473 env.deploy('mondogb')
1474 mock_juju.assert_called_with('deploy', ('mondogb',))
1475
1476 def test_deploy_joyent(self):
1477 env = EnvJujuClient1X(
1478 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1479- with patch.object(env, 'juju') as mock_juju:
1480+ with patch_juju_call(env) as mock_juju:
1481 env.deploy('mondogb')
1482 mock_juju.assert_called_with('deploy', ('mondogb',))
1483
1484 def test_deploy_repository(self):
1485 env = EnvJujuClient1X(
1486 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1487- with patch.object(env, 'juju') as mock_juju:
1488+ with patch_juju_call(env) as mock_juju:
1489 env.deploy('mondogb', '/home/jrandom/repo')
1490 mock_juju.assert_called_with(
1491 'deploy', ('mondogb', '--repository', '/home/jrandom/repo'))
1492@@ -835,7 +846,7 @@
1493 def test_deploy_to(self):
1494 env = EnvJujuClient1X(
1495 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1496- with patch.object(env, 'juju') as mock_juju:
1497+ with patch_juju_call(env) as mock_juju:
1498 env.deploy('mondogb', to='0')
1499 mock_juju.assert_called_with(
1500 'deploy', ('mondogb', '--to', '0'))
1501@@ -843,7 +854,7 @@
1502 def test_deploy_service(self):
1503 env = EnvJujuClient1X(
1504 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1505- with patch.object(env, 'juju') as mock_juju:
1506+ with patch_juju_call(env) as mock_juju:
1507 env.deploy('local:mondogb', service='my-mondogb')
1508 mock_juju.assert_called_with(
1509 'deploy', ('local:mondogb', 'my-mondogb',))
1510@@ -851,7 +862,7 @@
1511 def test_upgrade_charm(self):
1512 client = EnvJujuClient1X(
1513 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1514- with patch.object(client, 'juju') as mock_juju:
1515+ with patch_juju_call(client) as mock_juju:
1516 client.upgrade_charm('foo-service',
1517 '/bar/repository/angsty/mongodb')
1518 mock_juju.assert_called_once_with(
1519@@ -861,7 +872,7 @@
1520 def test_remove_service(self):
1521 client = EnvJujuClient1X(
1522 SimpleEnvironment('foo', {'type': 'local'}), '1.234-76', None)
1523- with patch.object(client, 'juju') as mock_juju:
1524+ with patch_juju_call(client) as mock_juju:
1525 client.remove_service('mondogb')
1526 mock_juju.assert_called_with('destroy-service', ('mondogb',))
1527
1528@@ -1348,7 +1359,7 @@
1529
1530 def test_set_model_constraints(self):
1531 client = EnvJujuClient1X(SimpleEnvironment('bar', {}), None, '/foo')
1532- with patch.object(client, 'juju') as juju_mock:
1533+ with patch_juju_call(client) as juju_mock:
1534 client.set_model_constraints({'bar': 'baz'})
1535 juju_mock.assert_called_once_with('set-constraints', ('bar=baz',))
1536
1537@@ -1592,7 +1603,7 @@
1538 def test_enable_ha(self):
1539 env = SimpleEnvironment('qux')
1540 client = EnvJujuClient1X(env, None, '/foobar/baz')
1541- with patch.object(client, 'juju', autospec=True) as eha_mock:
1542+ with patch_juju_call(client) as eha_mock:
1543 client.enable_ha()
1544 eha_mock.assert_called_once_with('ensure-availability', ('-n', '3'))
1545
1546@@ -1657,19 +1668,10 @@
1547 client.get_jes_command()
1548 self.assertEqual(0, po_mock.call_count)
1549
1550- def test_get_juju_timings(self):
1551- env = SimpleEnvironment('foo')
1552- client = EnvJujuClient1X(env, None, 'my/juju/bin')
1553- client._backend.juju_timings = {("juju", "op1"): [1],
1554- ("juju", "op2"): [2]}
1555- flattened_timings = client.get_juju_timings()
1556- expected = {"juju op1": [1], "juju op2": [2]}
1557- self.assertEqual(flattened_timings, expected)
1558-
1559 def test_deploy_bundle_1x(self):
1560 client = EnvJujuClient1X(SimpleEnvironment('an_env', None),
1561 '1.23-series-arch', None)
1562- with patch.object(client, 'juju') as mock_juju:
1563+ with patch_juju_call(client) as mock_juju:
1564 client.deploy_bundle('bundle:~juju-qa/some-bundle')
1565 mock_juju.assert_called_with(
1566 'deployer', ('--debug', '--deploy-delay', '10', '--timeout',
1567@@ -1678,7 +1680,7 @@
1568 def test_deploy_bundle_template(self):
1569 client = EnvJujuClient1X(SimpleEnvironment('an_env', None),
1570 '1.23-series-arch', None)
1571- with patch.object(client, 'juju') as mock_juju:
1572+ with patch_juju_call(client) as mock_juju:
1573 client.deploy_bundle('bundle:~juju-qa/some-{container}-bundle')
1574 mock_juju.assert_called_with(
1575 'deployer', (
1576@@ -1923,14 +1925,14 @@
1577 def test_add_space(self):
1578 client = EnvJujuClient1X(SimpleEnvironment(None, {'type': 'local'}),
1579 '1.23-series-arch', None)
1580- with patch.object(client, 'juju', autospec=True) as juju_mock:
1581+ with patch_juju_call(client) as juju_mock:
1582 client.add_space('foo-space')
1583 juju_mock.assert_called_once_with('space create', ('foo-space'))
1584
1585 def test_add_subnet(self):
1586 client = EnvJujuClient1X(SimpleEnvironment(None, {'type': 'local'}),
1587 '1.23-series-arch', None)
1588- with patch.object(client, 'juju', autospec=True) as juju_mock:
1589+ with patch_juju_call(client) as juju_mock:
1590 client.add_subnet('bar-subnet', 'foo-space')
1591 juju_mock.assert_called_once_with('subnet add',
1592 ('bar-subnet', 'foo-space'))
1593@@ -1944,7 +1946,7 @@
1594
1595 def test_set_config(self):
1596 client = EnvJujuClient1X(SimpleEnvironment('bar', {}), None, '/foo')
1597- with patch.object(client, 'juju') as juju_mock:
1598+ with patch_juju_call(client) as juju_mock:
1599 client.set_config('foo', {'bar': 'baz'})
1600 juju_mock.assert_called_once_with('set', ('foo', 'bar=baz'))
1601
1602@@ -2065,13 +2067,13 @@
1603
1604 def test_disable_command(self):
1605 client = EnvJujuClient1X(SimpleEnvironment('foo'), None, None)
1606- with patch.object(client, 'juju', autospec=True) as mock:
1607+ with patch_juju_call(client) as mock:
1608 client.disable_command('all', 'message')
1609 mock.assert_called_once_with('block all', ('message', ))
1610
1611 def test_enable_command(self):
1612 client = EnvJujuClient1X(SimpleEnvironment('foo'), None, None)
1613- with patch.object(client, 'juju', autospec=True) as mock:
1614+ with patch_juju_call(client) as mock:
1615 client.enable_command('all')
1616 mock.assert_called_once_with('unblock', 'all')
1617
1618
1619=== modified file 'jujupy/version_client.py'
1620--- jujupy/version_client.py 2017-03-27 15:15:05 +0000
1621+++ jujupy/version_client.py 2017-03-28 03:22:14 +0000
1622@@ -25,6 +25,7 @@
1623 import yaml
1624
1625 from jujupy.client import (
1626+ CommandComplete,
1627 Controller,
1628 _DEFAULT_BUNDLE_TIMEOUT,
1629 get_cache_path,
1630@@ -38,6 +39,7 @@
1631 LXC_MACHINE,
1632 make_safe_config,
1633 ModelClient,
1634+ NoopCondition,
1635 SimpleEnvironment,
1636 Status,
1637 StatusItem,
1638@@ -373,7 +375,8 @@
1639
1640 def set_model_constraints(self, constraints):
1641 constraint_strings = self._dict_as_option_strings(constraints)
1642- return self.juju('set-constraints', constraint_strings)
1643+ retvar, ct = self.juju('set-constraints', constraint_strings)
1644+ return retvar, CommandComplete(NoopCondition(), ct)
1645
1646 def set_config(self, service, options):
1647 option_strings = ['{}={}'.format(*item) for item in options.items()]
1648@@ -394,11 +397,13 @@
1649 def set_env_option(self, option, value):
1650 """Set the value of the option in the environment."""
1651 option_value = "%s=%s" % (option, value)
1652- return self.juju('set-env', (option_value,))
1653+ retvar, ct = self.juju('set-env', (option_value,))
1654+ return retvar, CommandComplete(NoopCondition(), ct)
1655
1656 def unset_env_option(self, option):
1657 """Unset the value of the option in the environment."""
1658- return self.juju('set-env', ('{}='.format(option),))
1659+ retvar, ct = self.juju('set-env', ('{}='.format(option),))
1660+ return retvar, CommandComplete(NoopCondition(), ct)
1661
1662 def get_model_defaults(self, model_key, cloud=None, region=None):
1663 log.info('No model-defaults stored for client (attempted get).')
1664@@ -449,7 +454,8 @@
1665 """Bootstrap a controller."""
1666 self._check_bootstrap()
1667 args = self.get_bootstrap_args(upload_tools, bootstrap_series)
1668- self.juju('bootstrap', args)
1669+ retvar, ct = self.juju('bootstrap', args)
1670+ return (0, CommandComplete(NoopCondition(), ct))
1671
1672 @contextmanager
1673 def bootstrap_async(self, upload_tools=False):
1674@@ -497,25 +503,27 @@
1675 """Destroy the environment, with force. Hard kill option.
1676
1677 :return: Subprocess's exit code."""
1678- return self.juju(
1679+ retvar, ct = self.juju(
1680 'destroy-environment', (self.env.environment, '--force', '-y'),
1681 check=check, include_e=False, timeout=get_teardown_timeout(self))
1682+ return retvar, CommandComplete(NoopCondition(), ct)
1683
1684 def destroy_controller(self, all_models=False):
1685 """Destroy the environment, with force. Soft kill option.
1686
1687 :param all_models: Ignored.
1688 :raises: subprocess.CalledProcessError if the operation fails."""
1689- return self.juju(
1690+ retvar, ct = self.juju(
1691 'destroy-environment', (self.env.environment, '-y'),
1692 include_e=False, timeout=get_teardown_timeout(self))
1693+ return retvar, CommandComplete(NoopCondition(), ct)
1694
1695 def destroy_environment(self, force=True, delete_jenv=False):
1696 if force:
1697 force_arg = ('--force',)
1698 else:
1699 force_arg = ()
1700- exit_status = self.juju(
1701+ exit_status, _ = self.juju(
1702 'destroy-environment',
1703 (self.env.environment,) + force_arg + ('-y',),
1704 check=False, include_e=False,
1705@@ -572,7 +580,8 @@
1706 args.extend(['--storage', storage])
1707 if constraints is not None:
1708 args.extend(['--constraints', constraints])
1709- return self.juju('deploy', tuple(args))
1710+ retvar, ct = self.juju('deploy', tuple(args))
1711+ return retvar, CommandComplete(NoopCondition(), ct)
1712
1713 def upgrade_charm(self, service, charm_path=None):
1714 args = (service,)
1715@@ -661,11 +670,13 @@
1716
1717 def disable_command(self, command_set, message=''):
1718 """Disable a command-set."""
1719- return self.juju('block {}'.format(command_set), (message, ))
1720+ retvar, ct = self.juju('block {}'.format(command_set), (message, ))
1721+ return retvar, CommandComplete(NoopCondition(), ct)
1722
1723 def enable_command(self, args):
1724 """Enable a command-set."""
1725- return self.juju('unblock', args)
1726+ retvar, ct = self.juju('unblock', args)
1727+ return retvar, CommandComplete(NoopCondition(), ct)
1728
1729
1730 class EnvJujuClient22(EnvJujuClient1X):
1731
1732=== modified file 'tests/__init__.py'
1733--- tests/__init__.py 2017-03-10 19:46:30 +0000
1734+++ tests/__init__.py 2017-03-28 03:22:14 +0000
1735@@ -20,6 +20,7 @@
1736 from unittest.mock import patch
1737 import yaml
1738
1739+from jujupy.client import CommandTime
1740 import utility
1741
1742
1743@@ -152,12 +153,25 @@
1744 os.environ[key] = org_value
1745
1746
1747+@contextmanager
1748+def patch_juju_call(client, return_value=0):
1749+ """Simple patch for client.juju call.
1750+
1751+ :param return_value: A tuple to return representing the retvar and
1752+ CommandTime object
1753+ """
1754+ with patch.object(
1755+ client, 'juju',
1756+ return_value=make_fake_juju_return(retvar=return_value)) as mock:
1757+ yield mock
1758+
1759+
1760 def assert_juju_call(test_case, mock_method, client, expected_args,
1761 call_index=None):
1762 """Check a mock's positional arguments.
1763
1764 :param test_case: The test case currently being run.
1765- :param mock_mothod: The mock object to be checked.
1766+ :param mock_method: The mock object to be checked.
1767 :param client: Ignored.
1768 :param expected_args: The expected positional arguments for the call.
1769 :param call_index: Index of the call to check, if None checks first call
1770@@ -248,3 +262,9 @@
1771 },
1772 }
1773 }
1774+
1775+
1776+def make_fake_juju_return(
1777+ retvar=0, cmd='mock_cmd', full_args=[], envvars=None, start=None):
1778+ """Shadow fake that defaults construction arguments."""
1779+ return (retvar, CommandTime(cmd, full_args, envvars, start))
1780
1781=== modified file 'tests/test_assess_block.py'
1782--- tests/test_assess_block.py 2017-01-20 20:49:41 +0000
1783+++ tests/test_assess_block.py 2017-03-28 03:22:14 +0000
1784@@ -18,6 +18,7 @@
1785 )
1786 from tests import (
1787 parse_error,
1788+ patch_juju_call,
1789 FakeHomeTestCase,
1790 TestCase,
1791 )
1792@@ -149,7 +150,7 @@
1793 autospec=True, side_effect=side_effects):
1794 with patch('assess_block.deploy_dummy_stack', autospec=True):
1795 with patch.object(client, 'remove_service') as mock_rs:
1796- with patch.object(client, 'juju') as mock_juju:
1797+ with patch_juju_call(client) as mock_juju:
1798 with patch.object(
1799 client, 'wait_for_started') as mock_ws:
1800 assess_block(client, 'trusty')
1801
1802=== modified file 'tests/test_assess_bootstrap.py'
1803--- tests/test_assess_bootstrap.py 2017-03-01 19:02:24 +0000
1804+++ tests/test_assess_bootstrap.py 2017-03-28 03:22:14 +0000
1805@@ -29,6 +29,7 @@
1806 from tests import (
1807 FakeHomeTestCase,
1808 TestCase,
1809+ make_fake_juju_return,
1810 )
1811 from utility import (
1812 JujuAssertionError,
1813@@ -361,7 +362,7 @@
1814 args.temp_env_name = 'qux'
1815 with extended_bootstrap_cxt('2.0.0'):
1816 with patch('jujupy.ModelClient.juju', autospec=True,
1817- side_effect=['', '']) as j_mock:
1818+ return_value=make_fake_juju_return()) as j_mock:
1819 with patch('assess_bootstrap.get_controller_hostname',
1820 return_value='test-host', autospec=True):
1821 bs_manager = BootstrapManager.from_args(args)
1822
1823=== modified file 'tests/test_assess_container_networking.py'
1824--- tests/test_assess_container_networking.py 2017-01-20 20:58:41 +0000
1825+++ tests/test_assess_container_networking.py 2017-03-28 03:22:14 +0000
1826@@ -17,6 +17,7 @@
1827 LXD_MACHINE,
1828 SimpleEnvironment,
1829 )
1830+from jujupy.client import CommandTime
1831
1832 import assess_container_networking as jcnet
1833 from tests import (
1834@@ -78,6 +79,29 @@
1835 if cmd != 'bootstrap':
1836 self.commands.append((cmd, args))
1837 if cmd == 'ssh':
1838+ ct = CommandTime(cmd, args)
1839+ if len(self._ssh_output) == 0:
1840+ return "", ct
1841+
1842+ try:
1843+ ct = CommandTime(cmd, args)
1844+ return self._ssh_output[self._call_number()], ct
1845+ except IndexError:
1846+ # If we ran out of values, just return the last one
1847+ return self._ssh_output[-1], ct
1848+ else:
1849+ return super(JujuMock, self).juju(cmd, *rargs, **kwargs)
1850+
1851+ def get_juju_output(self, cmd, *rargs, **kwargs):
1852+ # Almost exactly like juju() except get_juju_output doesn't return
1853+ # a CommandTime
1854+ if len(rargs) == 1:
1855+ args = rargs[0]
1856+ else:
1857+ args = rargs
1858+ if cmd != 'bootstrap':
1859+ self.commands.append((cmd, args))
1860+ if cmd == 'ssh':
1861 if len(self._ssh_output) == 0:
1862 return ""
1863
1864@@ -87,7 +111,7 @@
1865 # If we ran out of values, just return the last one
1866 return self._ssh_output[-1]
1867 else:
1868- return super(JujuMock, self).juju(cmd, *rargs, **kwargs)
1869+ return super(JujuMock, self).get_juju_output(cmd, *rargs, **kwargs)
1870
1871 def _call_number(self):
1872 call_number = self._call_n
1873@@ -117,7 +141,9 @@
1874 patch.object(self.client, 'wait_for', lambda *args, **kw: None),
1875 patch.object(self.client, 'wait_for_started',
1876 self.juju_mock.get_status),
1877- patch.object(self.client, 'get_juju_output', self.juju_mock.juju),
1878+ patch.object(
1879+ self.client, 'get_juju_output',
1880+ self.juju_mock.get_juju_output),
1881 ]
1882
1883 for patcher in patches:
1884
1885=== modified file 'tests/test_assess_user_grant_revoke.py'
1886--- tests/test_assess_user_grant_revoke.py 2017-03-23 19:23:19 +0000
1887+++ tests/test_assess_user_grant_revoke.py 2017-03-28 03:22:14 +0000
1888@@ -27,6 +27,7 @@
1889 from tests import (
1890 parse_error,
1891 TestCase,
1892+ patch_juju_call,
1893 )
1894
1895
1896@@ -150,7 +151,7 @@
1897 def test_assert_write_model(self):
1898 fake_client = fake_juju_client()
1899 with patch.object(fake_client, 'wait_for_started'):
1900- with patch.object(fake_client, 'juju', return_value=True):
1901+ with patch_juju_call(fake_client):
1902 assert_write_model(fake_client, 'write', True)
1903 with self.assertRaises(JujuAssertionError):
1904 assert_write_model(fake_client, 'write', False)
1905
1906=== modified file 'tests/test_deploy_stack.py'
1907--- tests/test_deploy_stack.py 2017-03-10 00:50:40 +0000
1908+++ tests/test_deploy_stack.py 2017-03-28 03:22:14 +0000
1909@@ -68,6 +68,11 @@
1910 Status,
1911 Machine,
1912 )
1913+
1914+from jujupy.client import (
1915+ NoopCondition,
1916+ CommandTime,
1917+)
1918 from jujupy.configuration import (
1919 get_environments_path,
1920 get_jenv_path,
1921@@ -82,6 +87,7 @@
1922 assert_juju_call,
1923 FakeHomeTestCase,
1924 FakePopen,
1925+ make_fake_juju_return,
1926 observable_temp_file,
1927 temp_os_env,
1928 use_context,
1929@@ -221,16 +227,38 @@
1930 self.assertEqual('region-foo', env.get_region())
1931
1932 def test_dump_juju_timings(self):
1933+ first_start = datetime(2017, 3, 22, 23, 36, 52, 0)
1934+ first_end = first_start + timedelta(seconds=2)
1935+ second_start = datetime(2017, 3, 22, 23, 40, 51, 0)
1936 env = JujuData('foo', {'type': 'bar'})
1937 client = ModelClient(env, None, None)
1938- client._backend.juju_timings = {("juju", "op1"): [1],
1939- ("juju", "op2"): [2]}
1940- expected = {"juju op1": [1], "juju op2": [2]}
1941+ client._backend.juju_timings.extend([
1942+ CommandTime('command1', ['command1', 'arg1'], start=first_start),
1943+ CommandTime(
1944+ 'command2', ['command2', 'arg1', 'arg2'], start=second_start)
1945+ ])
1946+ client._backend.juju_timings[0].actual_completion(end=first_end)
1947+ expected = [
1948+ {
1949+ 'command': 'command1',
1950+ 'full_args': ['command1', 'arg1'],
1951+ 'start': first_start,
1952+ 'end': first_end,
1953+ 'total_seconds': 2,
1954+ },
1955+ {
1956+ 'command': 'command2',
1957+ 'full_args': ['command2', 'arg1', 'arg2'],
1958+ 'start': second_start,
1959+ 'end': None,
1960+ 'total_seconds': None,
1961+ }
1962+ ]
1963 with temp_dir() as fake_dir:
1964 dump_juju_timings(client, fake_dir)
1965 with open(os.path.join(fake_dir,
1966- 'juju_command_times.json')) as out_file:
1967- file_data = json.load(out_file)
1968+ 'juju_command_times.yaml')) as out_file:
1969+ file_data = yaml.load(out_file)
1970 self.assertEqual(file_data, expected)
1971
1972 def test_check_token(self):
1973@@ -1757,7 +1785,7 @@
1974 def do_check(*args, **kwargs):
1975 with client.check_timeouts():
1976 with tear_down_client.check_timeouts():
1977- pass
1978+ return make_fake_juju_return()
1979
1980 with patch.object(bs_manager.tear_down_client, 'juju',
1981 side_effect=do_check, autospec=True):
1982@@ -1996,7 +2024,9 @@
1983 bs_manager.has_controller = False
1984 with patch('deploy_stack.safe_print_status',
1985 autospec=True) as sp_mock:
1986- with patch.object(client, 'juju', wrap=client.juju) as juju_mock:
1987+ with patch.object(
1988+ client, 'juju', wrap=client.juju,
1989+ return_value=make_fake_juju_return()) as juju_mock:
1990 with patch.object(client, 'get_juju_output',
1991 wraps=client.get_juju_output) as gjo_mock:
1992 with patch.object(bs_manager, '_should_dump',
1993@@ -2083,10 +2113,17 @@
1994 @contextmanager
1995 def booted_to_bootstrap(self, bs_manager):
1996 """Preform patches to focus on the call to bootstrap."""
1997+ # Need basic model details for get_status() (called in wait_for.)
1998+ bs_manager.client._backend.controller_state.add_model('controller')
1999+ bootstrap_return = (0, NoopCondition(already_satisfied=True))
2000 with patch.object(bs_manager, 'dump_all_logs'):
2001 with patch.object(bs_manager, 'runtime_context'):
2002- with patch.object(bs_manager.client, 'juju'):
2003- with patch.object(bs_manager.client, 'bootstrap') as mock:
2004+ with patch.object(
2005+ bs_manager.client, 'juju',
2006+ return_value=make_fake_juju_return()):
2007+ with patch.object(
2008+ bs_manager.client, 'bootstrap',
2009+ return_value=bootstrap_return) as mock:
2010 yield mock
2011
2012 def test_booted_context_kwargs(self):
2013@@ -2280,6 +2317,15 @@
2014 models = [{'name': 'controller'}, {'name': 'bar'}]
2015 self.addContext(patch.object(client, '_get_models',
2016 return_value=models, autospec=True))
2017+ # bootstrap now calls wait for.
2018+ m_controller_client = Mock()
2019+ m_controller_client.wait_for.return_value = {}
2020+ self.addContext(
2021+ patch.object(
2022+ client,
2023+ 'get_controller_client',
2024+ autospec=True,
2025+ return_value=m_controller_client))
2026 if jes:
2027 output = jes
2028 else:

Subscribers

People subscribed via source and target branches