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))