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

     1  """Testing helpers and base classes for better isolation."""
     2  
     3  from contextlib import contextmanager
     4  import datetime
     5  import logging
     6  import os
     7  import io
     8  try:
     9      from StringIO import StringIO
    10  except ImportError:
    11      from io import StringIO
    12  import subprocess
    13  import sys
    14  from tempfile import NamedTemporaryFile
    15  import unittest
    16  
    17  try:
    18      from mock import patch
    19  except ImportError:
    20      from unittest.mock import patch
    21  import yaml
    22  
    23  from jujupy.wait_condition import (
    24      CommandTime,
    25      )
    26  import utility
    27  
    28  
    29  @contextmanager
    30  def stdout_guard():
    31      if isinstance(sys.stdout, io.TextIOWrapper):
    32          stdout = io.StringIO()
    33      else:
    34          stdout = io.BytesIO()
    35      with patch('sys.stdout', stdout):
    36          yield
    37      if stdout.getvalue() != '':
    38          raise AssertionError(
    39              'Value written to stdout: {}'.format(stdout.getvalue()))
    40  
    41  
    42  def use_context(test_case, context):
    43      result = context.__enter__()
    44      test_case.addCleanup(context.__exit__, None, None, None)
    45      return result
    46  
    47  
    48  class TestCase(unittest.TestCase):
    49      """TestCase provides a better isolated version of unittest.TestCase."""
    50  
    51      log_level = logging.INFO
    52      test_environ = {}
    53  
    54      def setUp(self):
    55          super(TestCase, self).setUp()
    56  
    57          def _must_not_Popen(*args, **kwargs):
    58              """Tests may patch Popen but should never call it."""
    59              self.fail("subprocess.Popen(*{!r}, **{!r}) called".format(
    60                  args, kwargs))
    61  
    62          self.addCleanup(setattr, subprocess, "Popen", subprocess.Popen)
    63          subprocess.Popen = _must_not_Popen
    64  
    65          self.addCleanup(setattr, os, "environ", os.environ)
    66          os.environ = dict(self.test_environ)
    67  
    68          setup_test_logging(self, self.log_level)
    69  
    70      def assertIsTrue(self, expr, msg=None):
    71          """Assert that expr is the True object."""
    72          self.assertIs(expr, True, msg)
    73  
    74      def assertIsFalse(self, expr, msg=None):
    75          """Assert that expr is the False object."""
    76          self.assertIs(expr, False, msg)
    77  
    78      def addContext(self, context):
    79          """Enter context manager for the remainder of the test, then leave.
    80  
    81          This can be used in place of a with block in setUp, which must return
    82          and may not yield. Note that exceptions will not be passed in when
    83          calling __exit__."""
    84          self.addCleanup(context.__exit__, None, None, None)
    85          return context.__enter__()
    86  
    87  
    88  if getattr(TestCase, 'assertItemsEqual', None) is None:
    89      TestCase.assertItemsEqual = TestCase.assertCountEqual
    90  
    91  
    92  class FakeHomeTestCase(TestCase):
    93      """FakeHomeTestCase creates an isolated home dir for Juju to use."""
    94  
    95      def setUp(self):
    96          super(FakeHomeTestCase, self).setUp()
    97          self.home_dir = use_context(self, utility.temp_dir())
    98          os.environ['HOME'] = self.home_dir
    99          os.environ['PATH'] = os.path.join(self.home_dir, '.local', 'bin')
   100          self.juju_home = os.path.join(self.home_dir, '.juju')
   101          os.mkdir(self.juju_home)
   102          self.set_public_clouds(get_default_public_clouds())
   103  
   104      def set_public_clouds(self, data_dict):
   105          """Set the data in the public-clouds.yaml file.
   106  
   107          :param data_dict: A dictionary of data, which is used to overwrite
   108              the data in public-clouds.yaml, or None, in which case the file
   109              is removed."""
   110          dest_file = os.path.join(self.juju_home, 'public-clouds.yaml')
   111          if data_dict is None:
   112              with utility.skip_on_missing_file():
   113                  os.remove(dest_file)
   114          else:
   115              with open(dest_file, 'w') as file:
   116                  yaml.safe_dump(data_dict, file)
   117  
   118  
   119  def setup_test_logging(testcase, level=None):
   120      log = logging.getLogger()
   121      testcase.addCleanup(setattr, log, 'handlers', log.handlers)
   122      log.handlers = []
   123      testcase.log_stream = StringIO()
   124      handler = logging.StreamHandler(testcase.log_stream)
   125      handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
   126      log.addHandler(handler)
   127      if level is not None:
   128          testcase.addCleanup(log.setLevel, log.level)
   129          log.setLevel(level)
   130  
   131  
   132  # suppress nosetests
   133  setup_test_logging.__test__ = False
   134  
   135  
   136  @contextmanager
   137  def parse_error(test_case):
   138      if isinstance(sys.stdout, io.TextIOWrapper):
   139          stderr = io.StringIO()
   140      else:
   141          stderr = io.BytesIO()
   142      with test_case.assertRaises(SystemExit):
   143          with patch('sys.stderr', stderr):
   144              yield stderr
   145  
   146  
   147  @contextmanager
   148  def temp_os_env(key, value):
   149      """Set the environment key to value for the context, then restore it."""
   150      org_value = os.environ.get(key, '')
   151      os.environ[key] = value
   152      try:
   153          yield
   154      finally:
   155          os.environ[key] = org_value
   156  
   157  
   158  @contextmanager
   159  def patch_juju_call(client, return_value=0):
   160      """Simple patch for client.juju call.
   161  
   162      :param return_value: A tuple to return representing the retvar and
   163        CommandTime object
   164      """
   165      with patch.object(
   166              client, 'juju',
   167              return_value=make_fake_juju_return(retvar=return_value)) as mock:
   168          yield mock
   169  
   170  
   171  def assert_juju_call(test_case, mock_method, client, expected_args,
   172                       call_index=None):
   173      """Check a mock's positional arguments.
   174  
   175      :param test_case: The test case currently being run.
   176      :param mock_method: The mock object to be checked.
   177      :param client: Ignored.
   178      :param expected_args: The expected positional arguments for the call.
   179      :param call_index: Index of the call to check, if None checks first call
   180      and checks for only one call."""
   181      if call_index is None:
   182          test_case.assertEqual(len(mock_method.mock_calls), 1)
   183          call_index = 0
   184      empty, args, kwargs = mock_method.mock_calls[call_index]
   185      test_case.assertEqual(args, (expected_args,))
   186  
   187  
   188  class FakePopen(object):
   189      """Create an artifical version of the Popen class."""
   190  
   191      def __init__(self, out, err, returncode):
   192          self._out = out if out is None else out.encode('ascii')
   193          self._err = err if err is None else err.encode('ascii')
   194          self._code = returncode
   195  
   196      def communicate(self):
   197          self.returncode = self._code
   198          return self._out, self._err
   199  
   200      def poll(self):
   201          return self._code
   202  
   203  
   204  @contextmanager
   205  def observable_temp_file():
   206      """Get a name which is used to create temporary files in the context."""
   207      temporary_file = NamedTemporaryFile(delete=False)
   208      try:
   209          with temporary_file as temp_file:
   210  
   211              @contextmanager
   212              def nt():
   213                  # This is used to prevent NamedTemporaryFile.close from being
   214                  # called.
   215                  yield temporary_file
   216  
   217              with patch('jujupy.utility.NamedTemporaryFile',
   218                         return_value=nt()):
   219                  yield temp_file
   220      finally:
   221          # File may have already been deleted, e.g. by temp_yaml_file.
   222          with utility.skip_on_missing_file():
   223              os.unlink(temporary_file.name)
   224  
   225  
   226  @contextmanager
   227  def client_past_deadline(client):
   228      """Create a client patched to be past its deadline."""
   229      soft_deadline = datetime.datetime(2015, 1, 2, 3, 4, 6)
   230      now = soft_deadline + datetime.timedelta(seconds=1)
   231      old_soft_deadline = client._backend.soft_deadline
   232      client._backend.soft_deadline = soft_deadline
   233      try:
   234          with patch.object(client._backend, '_now', return_value=now,
   235                            autospec=True):
   236              yield client
   237      finally:
   238          client._backend.soft_deadline = old_soft_deadline
   239  
   240  
   241  def get_default_public_clouds():
   242      """The dict used to fill public-clouds.yaml by FakeHomeTestCase."""
   243      return {
   244          'clouds': {
   245              'foo': {
   246                  'type': 'foo',
   247                  'auth-types': ['access-key'],
   248                  'regions': {
   249                      # This is the fake juju endpoint:
   250                      'bar': {'endpoint': 'bar.foo.example.com'},
   251                      'fee': {'endpoint': 'fee.foo.example.com'},
   252                      'fi': {'endpoint': 'fi.foo.example.com'},
   253                      'foe': {'endpoint': 'foe.foo.example.com'},
   254                      'fum': {'endpoint': 'fum.foo.example.com'},
   255                      }
   256                  },
   257              'qux': {
   258                  'type': 'fake',
   259                  'auth-types': ['access-key'],
   260                  'regions': {
   261                      'north': {'endpoint': 'north.qux.example.com'},
   262                      'south': {'endpoint': 'south.qux.example.com'},
   263                      }
   264                  },
   265              }
   266          }
   267  
   268  
   269  def make_fake_juju_return(
   270          retvar=0, cmd='mock_cmd', full_args=[], envvars=None, start=None):
   271      """Shadow fake that defaults construction arguments."""
   272      return (retvar, CommandTime(cmd, full_args, envvars, start))