github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/tests/test_utility.py (about)

     1  from argparse import (
     2      ArgumentParser,
     3      Namespace,
     4      )
     5  from datetime import (
     6      datetime,
     7      timedelta,
     8      )
     9  import json
    10  import logging
    11  import os
    12  import socket
    13  from time import time
    14  
    15  try:
    16      from mock import (
    17          call,
    18          Mock,
    19          patch,
    20      )
    21  except ImportError:
    22      from unittest.mock import (
    23          call,
    24          Mock,
    25          patch,
    26      )
    27  from jujupy.utility import (
    28      temp_dir,
    29      )
    30  from tests import (
    31      TestCase,
    32      )
    33  from utility import (
    34      add_basic_testing_arguments,
    35      assert_dict_is_subset,
    36      as_literal_address,
    37      extract_deb,
    38      _find_candidates,
    39      find_candidates,
    40      find_latest_branch_candidates,
    41      get_candidates_path,
    42      get_deb_arch,
    43      get_winrm_certs,
    44      JujuAssertionError,
    45      log_and_wrap_exception,
    46      logged_exception,
    47      LoggedException,
    48      run_command,
    49      wait_for_port,
    50      )
    51  
    52  
    53  def write_config(root, job_name, token):
    54      job_dir = os.path.join(root, 'jobs', job_name)
    55      os.makedirs(job_dir)
    56      job_config = os.path.join(job_dir, 'config.xml')
    57      with open(job_config, 'w') as config:
    58          config.write(
    59              '<config><authToken>{}</authToken></config>'.format(token))
    60  
    61  
    62  class TestFindCandidates(TestCase):
    63  
    64      def test__find_candidates_artifacts_default(self):
    65          with temp_dir() as root:
    66              make_candidate_dir(root, 'master-artifacts')
    67              make_candidate_dir(root, '1.25')
    68              candidate = os.path.join(root, 'candidate', '1.25')
    69              self.assertEqual(list(_find_candidates(root)), [
    70                  (candidate, os.path.join(candidate, 'buildvars.json'))])
    71  
    72      def test__find_candidates_artifacts_enabled(self):
    73          with temp_dir() as root:
    74              make_candidate_dir(root, 'master-artifacts')
    75              make_candidate_dir(root, '1.25')
    76              candidate = os.path.join(root, 'candidate', 'master-artifacts')
    77              self.assertEqual(list(_find_candidates(root, artifacts=True)), [
    78                  (candidate, os.path.join(candidate, 'buildvars.json'))])
    79  
    80      def test_find_candidates(self):
    81          with temp_dir() as root:
    82              master_path = make_candidate_dir(root, 'master')
    83              self.assertEqual(list(find_candidates(root)), [master_path])
    84  
    85      def test_find_candidates_old_buildvars(self):
    86          with temp_dir() as root:
    87              a_week_ago = time() - timedelta(days=7, seconds=1).total_seconds()
    88              make_candidate_dir(root, 'master', modified=a_week_ago)
    89              self.assertEqual(list(find_candidates(root)), [])
    90  
    91      def test_find_candidates_artifacts(self):
    92          with temp_dir() as root:
    93              make_candidate_dir(root, 'master-artifacts')
    94              self.assertEqual(list(find_candidates(root)), [])
    95  
    96      def test_find_candidates_find_all(self):
    97          with temp_dir() as root:
    98              a_week_ago = time() - timedelta(days=7, seconds=1).total_seconds()
    99              master_path = make_candidate_dir(root, '1.23', modified=a_week_ago)
   100              master_path_2 = make_candidate_dir(root, '1.24')
   101              self.assertItemsEqual(list(find_candidates(root)), [master_path_2])
   102              self.assertItemsEqual(list(find_candidates(root, find_all=True)),
   103                                    [master_path, master_path_2])
   104  
   105  
   106  def make_candidate_dir(root, candidate_id, branch='foo', revision_build=1234,
   107                         modified=None):
   108      candidates_path = get_candidates_path(root)
   109      if not os.path.isdir(candidates_path):
   110          os.mkdir(candidates_path)
   111      master_path = os.path.join(candidates_path, candidate_id)
   112      os.mkdir(master_path)
   113      buildvars_path = os.path.join(master_path, 'buildvars.json')
   114      with open(buildvars_path, 'w') as buildvars_file:
   115          json.dump(
   116              {'branch': branch, 'revision_build': str(revision_build)},
   117              buildvars_file)
   118      if modified is not None:
   119          os.utime(buildvars_path, (time(), modified))
   120      juju_path = os.path.join(master_path, 'usr', 'foo', 'juju')
   121      os.makedirs(os.path.dirname(juju_path))
   122      with open(juju_path, 'w') as juju_file:
   123          juju_file.write('Fake juju bin.\n')
   124      return master_path
   125  
   126  
   127  class TestFindLatestBranchCandidates(TestCase):
   128  
   129      def test_find_latest_branch_candidates(self):
   130          with temp_dir() as root:
   131              master_path = make_candidate_dir(root, 'master-artifacts')
   132              self.assertEqual(find_latest_branch_candidates(root),
   133                               [(master_path, 1234)])
   134  
   135      def test_find_latest_branch_candidates_old_buildvars(self):
   136          with temp_dir() as root:
   137              a_week_ago = time() - timedelta(days=7, seconds=1).total_seconds()
   138              make_candidate_dir(root, 'master-artifacts', modified=a_week_ago)
   139              self.assertEqual(find_latest_branch_candidates(root), [])
   140  
   141      def test_ignore_older_revision_build(self):
   142          with temp_dir() as root:
   143              path_1234 = make_candidate_dir(
   144                  root, '1234-artifacts', 'mybranch', '1234')
   145              make_candidate_dir(root, '1233', 'mybranch', '1233')
   146              self.assertEqual(find_latest_branch_candidates(root), [
   147                  (path_1234, 1234)])
   148  
   149      def test_include_older_revision_build_different_branch(self):
   150          with temp_dir() as root:
   151              path_1234 = make_candidate_dir(
   152                  root, '1234-artifacts', 'branch_foo', '1234')
   153              path_1233 = make_candidate_dir(
   154                  root, '1233-artifacts', 'branch_bar', '1233')
   155              self.assertItemsEqual(
   156                  find_latest_branch_candidates(root), [
   157                      (path_1233, 1233), (path_1234, 1234)])
   158  
   159  
   160  class TestAsLiteralAddress(TestCase):
   161  
   162      def test_hostname(self):
   163          self.assertEqual("name.testing", as_literal_address("name.testing"))
   164  
   165      def test_ipv4(self):
   166          self.assertEqual("127.0.0.2", as_literal_address("127.0.0.2"))
   167  
   168      def test_ipv6(self):
   169          self.assertEqual("[2001:db8::7]", as_literal_address("2001:db8::7"))
   170  
   171  
   172  class TestWaitForPort(TestCase):
   173  
   174      def test_wait_for_port_0000_closed(self):
   175          with patch(
   176                  'socket.getaddrinfo', autospec=True,
   177                  return_value=[('foo', 'bar', 'baz', 'qux', ('0.0.0.0', 27))]
   178                  ) as gai_mock:
   179              with patch('socket.socket') as socket_mock:
   180                  wait_for_port('asdf', 26, closed=True)
   181          gai_mock.assert_called_once_with('asdf', 26, socket.AF_INET,
   182                                           socket.SOCK_STREAM)
   183          self.assertEqual(socket_mock.call_count, 0)
   184  
   185      def test_wait_for_port_0000_open(self):
   186          stub_called = False
   187          loc = locals()
   188  
   189          def gai_stub(host, port, family, socktype):
   190              if loc['stub_called']:
   191                  raise ValueError()
   192              loc['stub_called'] = True
   193              return [('foo', 'bar', 'baz', 'qux', ('0.0.0.0', 27))]
   194  
   195          with patch('socket.getaddrinfo', autospec=True, side_effect=gai_stub,
   196                     ) as gai_mock:
   197              with patch('socket.socket') as socket_mock:
   198                  with self.assertRaises(ValueError):
   199                      wait_for_port('asdf', 26, closed=False)
   200          self.assertEqual(gai_mock.mock_calls, [
   201              call('asdf', 26, socket.AF_INET, socket.SOCK_STREAM),
   202              call('asdf', 26, socket.AF_INET, socket.SOCK_STREAM),
   203              ])
   204          self.assertEqual(socket_mock.call_count, 0)
   205  
   206      def test_wait_for_port(self):
   207          with patch(
   208                  'socket.getaddrinfo', autospec=True, return_value=[
   209                      ('foo', 'bar', 'baz', 'qux', ('192.168.8.3', 27))
   210                      ]) as gai_mock:
   211              with patch('socket.socket') as socket_mock:
   212                  wait_for_port('asdf', 26, closed=False)
   213          gai_mock.assert_called_once_with(
   214              'asdf', 26, socket.AF_INET, socket.SOCK_STREAM),
   215          socket_mock.assert_called_once_with('foo', 'bar', 'baz')
   216          connect_mock = socket_mock.return_value.connect
   217          connect_mock.assert_called_once_with(('192.168.8.3', 27))
   218  
   219      def test_wait_for_port_no_address_closed(self):
   220          error = socket.gaierror(socket.EAI_NODATA, 'What address?')
   221          with patch('socket.getaddrinfo', autospec=True,
   222                     side_effect=error) as gai_mock:
   223              with patch('socket.socket') as socket_mock:
   224                  wait_for_port('asdf', 26, closed=True)
   225          gai_mock.assert_called_once_with('asdf', 26, socket.AF_INET,
   226                                           socket.SOCK_STREAM)
   227          self.assertEqual(socket_mock.call_count, 0)
   228  
   229      def test_wait_for_port_no_address_open(self):
   230          stub_called = False
   231          loc = locals()
   232  
   233          def gai_stub(host, port, family, socktype):
   234              if loc['stub_called']:
   235                  raise ValueError()
   236              loc['stub_called'] = True
   237              raise socket.error(socket.EAI_NODATA, 'Err, address?')
   238  
   239          with patch('socket.getaddrinfo', autospec=True, side_effect=gai_stub,
   240                     ) as gai_mock:
   241              with patch('socket.socket') as socket_mock:
   242                  with self.assertRaises(ValueError):
   243                      wait_for_port('asdf', 26, closed=False)
   244          self.assertEqual(gai_mock.mock_calls, [
   245              call('asdf', 26, socket.AF_INET, socket.SOCK_STREAM),
   246              call('asdf', 26, socket.AF_INET, socket.SOCK_STREAM),
   247              ])
   248          self.assertEqual(socket_mock.call_count, 0)
   249  
   250      def test_ipv6_open(self):
   251          gai_result = [(23, 0, 0, '', ('2001:db8::2', 22, 0, 0))]
   252          with patch('socket.getaddrinfo', autospec=True,
   253                     return_value=gai_result) as gai_mock:
   254              with patch('socket.socket') as socket_mock:
   255                  wait_for_port('2001:db8::2', 22, closed=False)
   256          gai_mock.assert_called_once_with(
   257              '2001:db8::2', 22, socket.AF_INET6, socket.SOCK_STREAM)
   258          socket_mock.assert_called_once_with(23, 0, 0)
   259          connect_mock = socket_mock.return_value.connect
   260          connect_mock.assert_called_once_with(('2001:db8::2', 22, 0, 0))
   261  
   262  
   263  class TestExtractDeb(TestCase):
   264  
   265      def test_extract_deb(self):
   266          with patch('subprocess.check_call', autospec=True) as cc_mock:
   267              extract_deb('foo', 'bar')
   268          cc_mock.assert_called_once_with(['dpkg', '-x', 'foo', 'bar'])
   269  
   270  
   271  class TestGetDebArch(TestCase):
   272  
   273      def test_get_deb_arch(self):
   274          with patch('subprocess.check_output',
   275                     return_value=' amd42 \n') as co_mock:
   276              arch = get_deb_arch()
   277          co_mock.assert_called_once_with(['dpkg', '--print-architecture'])
   278          self.assertEqual(arch, 'amd42')
   279  
   280  
   281  class TestAddBasicTestingArguments(TestCase):
   282  
   283      def test_no_args(self):
   284          cmd_line = []
   285          parser = add_basic_testing_arguments(ArgumentParser(),
   286                                               deadline=True)
   287          args = parser.parse_args(cmd_line)
   288          self.assertEqual(args.env, 'lxd')
   289          self.assertEqual(args.juju_bin, None)
   290  
   291          self.assertEqual(args.logs, None)
   292  
   293          temp_env_name_arg = args.temp_env_name.split("-")
   294          temp_env_name_ts = temp_env_name_arg[1]
   295          self.assertEqual(temp_env_name_arg[0:1], ['testutility'])
   296          self.assertTrue(temp_env_name_ts,
   297                          datetime.strptime(temp_env_name_ts, "%Y%m%d%H%M%S"))
   298          self.assertEqual(temp_env_name_arg[2:4], ['temp', 'env'])
   299          self.assertIs(None, args.deadline)
   300  
   301      def test_positional_args(self):
   302          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest']
   303          parser = add_basic_testing_arguments(ArgumentParser(), deadline=True)
   304          args = parser.parse_args(cmd_line)
   305          expected = Namespace(
   306              agent_url=None, debug=False, env='lxd', temp_env_name='testtest',
   307              juju_bin='/foo/juju', logs='/tmp/logs', series=None,
   308              verbose=logging.INFO, agent_stream=None, keep_env=False,
   309              upload_tools=False, bootstrap_host=None, machine=[], region=None,
   310              deadline=None, to=None, existing=None)
   311          self.assertEqual(args, expected)
   312  
   313      def test_positional_args_add_juju_bin_name(self):
   314          cmd_line = ['lxd', '/juju', '/tmp/logs', 'testtest']
   315          parser = add_basic_testing_arguments(ArgumentParser(), deadline=True)
   316          args = parser.parse_args(cmd_line)
   317          self.assertEqual(args.juju_bin, '/juju')
   318  
   319      def test_positional_args_accepts_juju_exe(self):
   320          cmd_line = ['lxd', 'c:\\juju.exe', '/tmp/logs', 'testtest']
   321          parser = add_basic_testing_arguments(ArgumentParser(), deadline=True)
   322          args = parser.parse_args(cmd_line)
   323          self.assertEqual(args.juju_bin, 'c:\\juju.exe')
   324  
   325      def test_warns_on_dirty_logs(self):
   326          with temp_dir() as log_dir:
   327              open(os.path.join(log_dir, "existing.log"), "w").close()
   328              cmd_line = ['lxd', '/a/juju', log_dir, 'testtest']
   329              parser = add_basic_testing_arguments(ArgumentParser())
   330              parser.parse_args(cmd_line)
   331          self.assertIn('has existing contents', self.log_stream.getvalue())
   332  
   333      def test_no_warn_on_empty_logs(self):
   334          """Special case a file named 'empty' doesn't make log dir dirty"""
   335          with temp_dir() as log_dir:
   336              open(os.path.join(log_dir, "empty"), "w").close()
   337              cmd_line = ['lxd', '/a/juju', log_dir, 'testtest']
   338              parser = add_basic_testing_arguments(ArgumentParser())
   339              parser.parse_args(cmd_line)
   340          self.assertEqual("", self.log_stream.getvalue())
   341  
   342      def test_warn_on_nonexistent_directory_creation(self):
   343          log_dir = '/x/y/nothing'
   344          cmd_line = ['lxd', '/foo/juju', log_dir, 'testtest']
   345          parser = add_basic_testing_arguments(ArgumentParser())
   346          parser.parse_args(cmd_line)
   347          self.assertIn('Not a directory', self.log_stream.getvalue())
   348  
   349      def test_debug(self):
   350          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest', '--debug']
   351          parser = add_basic_testing_arguments(ArgumentParser())
   352          args = parser.parse_args(cmd_line)
   353          self.assertEqual(args.debug, True)
   354  
   355      def test_verbose_logging(self):
   356          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest', '--verbose']
   357          parser = add_basic_testing_arguments(ArgumentParser())
   358          args = parser.parse_args(cmd_line)
   359          self.assertEqual(args.verbose, logging.DEBUG)
   360  
   361      def test_agent_url(self):
   362          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest',
   363                      '--agent-url', 'http://example.org']
   364          parser = add_basic_testing_arguments(ArgumentParser())
   365          args = parser.parse_args(cmd_line)
   366          self.assertEqual(args.agent_url, 'http://example.org')
   367  
   368      def test_agent_stream(self):
   369          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest',
   370                      '--agent-stream', 'testing']
   371          parser = add_basic_testing_arguments(ArgumentParser())
   372          args = parser.parse_args(cmd_line)
   373          self.assertEqual(args.agent_stream, 'testing')
   374  
   375      def test_series(self):
   376          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest', '--series',
   377                      'vivid']
   378          parser = add_basic_testing_arguments(ArgumentParser())
   379          args = parser.parse_args(cmd_line)
   380          self.assertEqual(args.series, 'vivid')
   381  
   382      def test_upload_tools(self):
   383          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest',
   384                      '--upload-tools']
   385          parser = add_basic_testing_arguments(ArgumentParser())
   386          args = parser.parse_args(cmd_line)
   387          self.assertTrue(args.upload_tools)
   388  
   389      def test_using_jes_upload_tools(self):
   390          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest',
   391                      '--upload-tools']
   392          parser = add_basic_testing_arguments(ArgumentParser(), using_jes=True)
   393          with patch.object(parser, 'error') as mock_error:
   394              parser.parse_args(cmd_line)
   395          mock_error.assert_called_once_with(
   396              'unrecognized arguments: --upload-tools')
   397  
   398      def test_bootstrap_host(self):
   399          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest',
   400                      '--bootstrap-host', 'bar']
   401          parser = add_basic_testing_arguments(ArgumentParser())
   402          args = parser.parse_args(cmd_line)
   403          self.assertEqual(args.bootstrap_host, 'bar')
   404  
   405      def test_machine(self):
   406          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest',
   407                      '--machine', 'bar', '--machine', 'baz']
   408          parser = add_basic_testing_arguments(ArgumentParser())
   409          args = parser.parse_args(cmd_line)
   410          self.assertEqual(args.machine, ['bar', 'baz'])
   411  
   412      def test_keep_env(self):
   413          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest',
   414                      '--keep-env']
   415          parser = add_basic_testing_arguments(ArgumentParser())
   416          args = parser.parse_args(cmd_line)
   417          self.assertTrue(args.keep_env)
   418  
   419      def test_region(self):
   420          cmd_line = ['lxd', '/foo/juju', '/tmp/logs', 'testtest',
   421                      '--region', 'foo-bar']
   422          parser = add_basic_testing_arguments(ArgumentParser())
   423          args = parser.parse_args(cmd_line)
   424          self.assertEqual('foo-bar', args.region)
   425  
   426      def test_deadline(self):
   427          now = datetime(2012, 11, 10, 9, 8, 7)
   428          cmd_line = ['--timeout', '300']
   429          parser = add_basic_testing_arguments(ArgumentParser(), deadline=True)
   430          with patch('utility.datetime') as dt_class:
   431              # Can't patch the utcnow method of datetime.datetime (because it's
   432              # C code?) but we can patch out the whole datetime class.
   433              dt_class.utcnow.return_value = now
   434              args = parser.parse_args(cmd_line)
   435          self.assertEqual(now + timedelta(seconds=300), args.deadline)
   436  
   437      def test_no_env(self):
   438          cmd_line = ['/foo/juju', '/tmp/logs', 'testtest']
   439          parser = add_basic_testing_arguments(ArgumentParser(), env=False)
   440          args = parser.parse_args(cmd_line)
   441          expected = Namespace(
   442              agent_url=None, debug=False, temp_env_name='testtest',
   443              juju_bin='/foo/juju', logs='/tmp/logs', series=None,
   444              verbose=logging.INFO, agent_stream=None, keep_env=False,
   445              upload_tools=False, bootstrap_host=None, machine=[], region=None,
   446              deadline=None, to=None, existing=None)
   447          self.assertEqual(args, expected)
   448  
   449  
   450  class TestRunCommand(TestCase):
   451  
   452      def test_run_command_args(self):
   453          with patch('subprocess.check_output') as co_mock:
   454              run_command(['foo', 'bar'])
   455          args, kwargs = co_mock.call_args
   456          self.assertEqual((['foo', 'bar'], ), args)
   457  
   458      def test_run_command_dry_run(self):
   459          with patch('subprocess.check_output') as co_mock:
   460              run_command(['foo', 'bar'], dry_run=True)
   461              self.assertEqual(0, co_mock.call_count)
   462  
   463      def test_run_command_verbose(self):
   464          with patch('subprocess.check_output'):
   465              with patch('utility.print_now') as p_mock:
   466                  run_command(['foo', 'bar'], verbose=True)
   467                  self.assertEqual(2, p_mock.call_count)
   468  
   469  
   470  class TestGetWinRmCerts(TestCase):
   471  
   472      def test_get_certs(self):
   473          with patch.dict(os.environ, {"HOME": "/fake/home"}):
   474              certs = get_winrm_certs()
   475          self.assertEqual(certs, (
   476              "/fake/home/cloud-city/winrm_client_cert.key",
   477              "/fake/home/cloud-city/winrm_client_cert.pem",
   478          ))
   479  
   480  
   481  class TestLogAndWrapException(TestCase):
   482  
   483      def test_exception(self):
   484          mock_logger = Mock(spec=['exception'])
   485          err = Exception('an error')
   486          wrapped = log_and_wrap_exception(mock_logger, err)
   487          self.assertIs(wrapped.exception, err)
   488          mock_logger.exception.assert_called_once_with(err)
   489  
   490      def test_has_stdout(self):
   491          mock_logger = Mock(spec=['exception', 'info'])
   492          err = Exception('another error')
   493          err.output = 'stdout text'
   494          wrapped = log_and_wrap_exception(mock_logger, err)
   495          self.assertIs(wrapped.exception, err)
   496          mock_logger.exception.assert_called_once_with(err)
   497          mock_logger.info.assert_called_once_with(
   498              'Output from exception:\nstdout:\n%s\nstderr:\n%s', 'stdout text',
   499              None)
   500  
   501      def test_has_stderr(self):
   502          mock_logger = Mock(spec=['exception', 'info'])
   503          err = Exception('another error')
   504          err.stderr = 'stderr text'
   505          wrapped = log_and_wrap_exception(mock_logger, err)
   506          self.assertIs(wrapped.exception, err)
   507          mock_logger.exception.assert_called_once_with(err)
   508          mock_logger.info.assert_called_once_with(
   509              'Output from exception:\nstdout:\n%s\nstderr:\n%s', None,
   510              'stderr text')
   511  
   512  
   513  class TestLoggedException(TestCase):
   514  
   515      def test_no_error_no_log(self):
   516          mock_logger = Mock(spec_set=[])
   517          with logged_exception(mock_logger):
   518              pass
   519  
   520      def test_exception_logged_and_wrapped(self):
   521          mock_logger = Mock(spec=['exception'])
   522          err = Exception('some error')
   523          with self.assertRaises(LoggedException) as ctx:
   524              with logged_exception(mock_logger):
   525                  raise err
   526          self.assertIs(ctx.exception.exception, err)
   527          mock_logger.exception.assert_called_once_with(err)
   528  
   529      def test_exception_logged_once(self):
   530          mock_logger = Mock(spec=['exception'])
   531          err = Exception('another error')
   532          with self.assertRaises(LoggedException) as ctx:
   533              with logged_exception(mock_logger):
   534                  with logged_exception(mock_logger):
   535                      raise err
   536          self.assertIs(ctx.exception.exception, err)
   537          mock_logger.exception.assert_called_once_with(err)
   538  
   539      def test_generator_exit_not_wrapped(self):
   540          mock_logger = Mock(spec_set=[])
   541          with self.assertRaises(GeneratorExit):
   542              with logged_exception(mock_logger):
   543                  raise GeneratorExit
   544  
   545      def test_keyboard_interrupt_wrapped(self):
   546          mock_logger = Mock(spec=['exception'])
   547          err = KeyboardInterrupt()
   548          with self.assertRaises(LoggedException) as ctx:
   549              with logged_exception(mock_logger):
   550                  raise err
   551          self.assertIs(ctx.exception.exception, err)
   552          mock_logger.exception.assert_called_once_with(err)
   553  
   554      def test_output_logged(self):
   555          mock_logger = Mock(spec=['exception', 'info'])
   556          err = Exception('some error')
   557          err.output = 'some output'
   558          with self.assertRaises(LoggedException) as ctx:
   559              with logged_exception(mock_logger):
   560                  raise err
   561          self.assertIs(ctx.exception.exception, err)
   562          mock_logger.exception.assert_called_once_with(err)
   563          mock_logger.info.assert_called_once_with(
   564              'Output from exception:\nstdout:\n%s\nstderr:\n%s', 'some output',
   565              None)
   566  
   567  
   568  class TestAssertDictIsSubset(TestCase):
   569  
   570      def test_assert_dict_is_subset(self):
   571          # Identical dicts.
   572          self.assertIsTrue(
   573              assert_dict_is_subset(
   574                  {'a': 1, 'b': 2},
   575                  {'a': 1, 'b': 2}))
   576          # super dict has an extra item.
   577          self.assertIsTrue(
   578              assert_dict_is_subset(
   579                  {'a': 1, 'b': 2},
   580                  {'a': 1, 'b': 2, 'c': 3}))
   581          # A key is missing.
   582          with self.assertRaises(JujuAssertionError):
   583              assert_dict_is_subset(
   584                  {'a': 1, 'b': 2},
   585                  {'a': 1, 'c': 2})
   586          # A value is different.
   587          with self.assertRaises(JujuAssertionError):
   588              assert_dict_is_subset(
   589                  {'a': 1, 'b': 2},
   590                  {'a': 1, 'b': 4})