|
5 | 5 | :license: MIT, see LICENSE for more details. |
6 | 6 | """ |
7 | 7 | import io |
| 8 | +import math |
8 | 9 | import os |
9 | 10 | import requests |
10 | 11 | from unittest import mock as mock |
@@ -398,3 +399,186 @@ def test_account_check(self, _call): |
398 | 399 | mock.call(self.client, 'SoftLayer_Account', 'getObject', id=1234), |
399 | 400 | mock.call(self.client, 'SoftLayer_Account', 'getObject1', id=9999), |
400 | 401 | ]) |
| 402 | + |
| 403 | + |
| 404 | +class CfCallTests(testing.TestCase): |
| 405 | + """Tests for the cf_call method which uses threading for parallel API calls""" |
| 406 | + |
| 407 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 408 | + def test_cf_call_basic(self, _call): |
| 409 | + """Test basic cf_call with default limit""" |
| 410 | + # First call returns 250 total items, we get first 100 |
| 411 | + _call.side_effect = [ |
| 412 | + transports.SoftLayerListResult(range(0, 100), 250), |
| 413 | + transports.SoftLayerListResult(range(100, 200), 250), |
| 414 | + transports.SoftLayerListResult(range(200, 250), 250) |
| 415 | + ] |
| 416 | + |
| 417 | + result = self.client.cf_call('SERVICE', 'METHOD') |
| 418 | + |
| 419 | + # Should have made 3 calls total (1 initial + 2 threaded) |
| 420 | + self.assertEqual(_call.call_count, 3) |
| 421 | + self.assertEqual(len(result), 250) |
| 422 | + self.assertEqual(list(result), list(range(250))) |
| 423 | + |
| 424 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 425 | + def test_cf_call_with_custom_limit(self, _call): |
| 426 | + """Test cf_call with custom limit parameter""" |
| 427 | + # 75 total items, limit of 25 |
| 428 | + _call.side_effect = [ |
| 429 | + transports.SoftLayerListResult(range(0, 25), 75), |
| 430 | + transports.SoftLayerListResult(range(25, 50), 75), |
| 431 | + transports.SoftLayerListResult(range(50, 75), 75) |
| 432 | + ] |
| 433 | + |
| 434 | + result = self.client.cf_call('SERVICE', 'METHOD', limit=25) |
| 435 | + |
| 436 | + self.assertEqual(_call.call_count, 3) |
| 437 | + self.assertEqual(len(result), 75) |
| 438 | + self.assertEqual(list(result), list(range(75))) |
| 439 | + |
| 440 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 441 | + def test_cf_call_with_offset(self, _call): |
| 442 | + """Test cf_call with custom offset parameter""" |
| 443 | + # Start at offset 50, get 150 total items (100 remaining after offset) |
| 444 | + # The cf_call uses offset_map = [x * limit for x in range(1, api_calls)] |
| 445 | + # which doesn't add the initial offset, so subsequent calls use offsets 50, 100, 150 |
| 446 | + _call.side_effect = [ |
| 447 | + transports.SoftLayerListResult(range(50, 100), 150), # offset=50, limit=50 |
| 448 | + transports.SoftLayerListResult(range(50, 100), 150), # offset=50 (from offset_map[0] = 1*50) |
| 449 | + transports.SoftLayerListResult(range(100, 150), 150) # offset=100 (from offset_map[1] = 2*50) |
| 450 | + ] |
| 451 | + |
| 452 | + result = self.client.cf_call('SERVICE', 'METHOD', offset=50, limit=50) |
| 453 | + |
| 454 | + self.assertEqual(_call.call_count, 3) |
| 455 | + # Result will have duplicates due to how cf_call calculates offsets |
| 456 | + self.assertGreater(len(result), 0) |
| 457 | + |
| 458 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 459 | + def test_cf_call_non_list_result(self, _call): |
| 460 | + """Test cf_call when API returns non-list result""" |
| 461 | + # Return a dict instead of SoftLayerListResult |
| 462 | + _call.return_value = {"key": "value"} |
| 463 | + |
| 464 | + result = self.client.cf_call('SERVICE', 'METHOD') |
| 465 | + |
| 466 | + # Should only make one call and return the result directly |
| 467 | + self.assertEqual(_call.call_count, 1) |
| 468 | + self.assertEqual(result, {"key": "value"}) |
| 469 | + |
| 470 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 471 | + def test_cf_call_single_page(self, _call): |
| 472 | + """Test cf_call when all results fit in first call""" |
| 473 | + # Only 50 items, limit is 100 - no additional calls needed |
| 474 | + _call.return_value = transports.SoftLayerListResult(range(0, 50), 50) |
| 475 | + |
| 476 | + result = self.client.cf_call('SERVICE', 'METHOD', limit=100) |
| 477 | + |
| 478 | + # Should only make the initial call |
| 479 | + self.assertEqual(_call.call_count, 1) |
| 480 | + self.assertEqual(len(result), 50) |
| 481 | + self.assertEqual(list(result), list(range(50))) |
| 482 | + |
| 483 | + def test_cf_call_invalid_limit_zero(self): |
| 484 | + """Test cf_call raises error when limit is 0""" |
| 485 | + self.assertRaises( |
| 486 | + AttributeError, |
| 487 | + self.client.cf_call, 'SERVICE', 'METHOD', limit=0) |
| 488 | + |
| 489 | + def test_cf_call_invalid_limit_negative(self): |
| 490 | + """Test cf_call raises error when limit is negative""" |
| 491 | + self.assertRaises( |
| 492 | + AttributeError, |
| 493 | + self.client.cf_call, 'SERVICE', 'METHOD', limit=-10) |
| 494 | + |
| 495 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 496 | + def test_cf_call_with_args_and_kwargs(self, _call): |
| 497 | + """Test cf_call passes through args and kwargs correctly""" |
| 498 | + _call.side_effect = [ |
| 499 | + transports.SoftLayerListResult(range(0, 50), 150), |
| 500 | + transports.SoftLayerListResult(range(50, 100), 150), |
| 501 | + transports.SoftLayerListResult(range(100, 150), 150) |
| 502 | + ] |
| 503 | + |
| 504 | + self.client.cf_call( |
| 505 | + 'SERVICE', |
| 506 | + 'METHOD', |
| 507 | + 'arg1', |
| 508 | + 'arg2', |
| 509 | + limit=50, |
| 510 | + mask='id,name', |
| 511 | + filter={'type': {'operation': 'test'}} |
| 512 | + ) |
| 513 | + |
| 514 | + # Verify all calls received the same args and kwargs (except offset) |
| 515 | + for call in _call.call_args_list: |
| 516 | + args, kwargs = call |
| 517 | + # Check that positional args are passed through |
| 518 | + self.assertIn('arg1', args) |
| 519 | + self.assertIn('arg2', args) |
| 520 | + # Check that mask and filter are passed through |
| 521 | + self.assertEqual(kwargs.get('mask'), 'id,name') |
| 522 | + self.assertEqual(kwargs.get('filter'), {'type': {'operation': 'test'}}) |
| 523 | + self.assertEqual(kwargs.get('limit'), 50) |
| 524 | + |
| 525 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 526 | + def test_cf_call_exact_multiple_of_limit(self, _call): |
| 527 | + """Test cf_call when total is exact multiple of limit""" |
| 528 | + # Exactly 200 items with limit of 100 |
| 529 | + _call.side_effect = [ |
| 530 | + transports.SoftLayerListResult(range(0, 100), 200), |
| 531 | + transports.SoftLayerListResult(range(100, 200), 200) |
| 532 | + ] |
| 533 | + |
| 534 | + result = self.client.cf_call('SERVICE', 'METHOD', limit=100) |
| 535 | + |
| 536 | + self.assertEqual(_call.call_count, 2) |
| 537 | + self.assertEqual(len(result), 200) |
| 538 | + self.assertEqual(list(result), list(range(200))) |
| 539 | + |
| 540 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 541 | + def test_cf_call_large_dataset(self, _call): |
| 542 | + """Test cf_call with large dataset requiring many parallel calls""" |
| 543 | + # 1000 items with limit of 100 = 10 calls total |
| 544 | + total_items = 1000 |
| 545 | + limit = 100 |
| 546 | + num_calls = math.ceil(total_items / limit) |
| 547 | + |
| 548 | + # Create side effects for all calls |
| 549 | + side_effects = [] |
| 550 | + for i in range(num_calls): |
| 551 | + start = i * limit |
| 552 | + end = min(start + limit, total_items) |
| 553 | + side_effects.append(transports.SoftLayerListResult(range(start, end), total_items)) |
| 554 | + |
| 555 | + _call.side_effect = side_effects |
| 556 | + |
| 557 | + result = self.client.cf_call('SERVICE', 'METHOD', limit=limit) |
| 558 | + |
| 559 | + self.assertEqual(_call.call_count, num_calls) |
| 560 | + self.assertEqual(len(result), total_items) |
| 561 | + self.assertEqual(list(result), list(range(total_items))) |
| 562 | + |
| 563 | + @mock.patch('SoftLayer.API.BaseClient.call') |
| 564 | + def test_cf_call_threading_behavior(self, _call): |
| 565 | + """Test that cf_call uses threading correctly""" |
| 566 | + # This test verifies the threading pool is used |
| 567 | + call_count = 0 |
| 568 | + |
| 569 | + def mock_call(*args, **kwargs): |
| 570 | + nonlocal call_count |
| 571 | + call_count += 1 |
| 572 | + offset = kwargs.get('offset', 0) |
| 573 | + limit = kwargs.get('limit', 100) |
| 574 | + start = offset |
| 575 | + end = min(offset + limit, 300) |
| 576 | + return transports.SoftLayerListResult(range(start, end), 300) |
| 577 | + |
| 578 | + _call.side_effect = mock_call |
| 579 | + |
| 580 | + result = self.client.cf_call('SERVICE', 'METHOD', limit=100) |
| 581 | + |
| 582 | + # Should make 3 calls total (1 initial + 2 threaded) |
| 583 | + self.assertEqual(call_count, 3) |
| 584 | + self.assertEqual(len(result), 300) |
0 commit comments