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