test_aws_pagination.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """
  2. Unit tests for ``BotoEC2Service`` server-side pagination.
  3. ``_get_paginated_results`` hands boto3 a ``PaginationConfig`` built from two
  4. separate numbers: ``MaxItems``, how many results the caller asked for, and
  5. ``PageSize``, how many the service returns per request. They used to be the
  6. same value, which made a small result limit walk a large scan in tiny
  7. increments - a filtered ``describe_images`` measured 977.6s at a page size of
  8. 5 against 10.0s at 1000, for the same single result.
  9. Separating them moves where page boundaries fall, and the resume token
  10. handed back to callers is defined in terms of those boundaries. These tests
  11. pin the round trip: paging a collection with a small limit must still visit
  12. every object exactly once and terminate, whatever the transport page size.
  13. Exercised against moto so they run in CI without cloud credentials.
  14. """
  15. import unittest
  16. from moto import mock_aws
  17. from cloudbridge.factory import CloudProviderFactory
  18. from cloudbridge.factory import ProviderList
  19. from cloudbridge.providers.aws.helpers import DEFAULT_PAGE_SIZE
  20. # More objects than one page at the result limit below, so paging is forced.
  21. OBJECT_COUNT = 7
  22. RESULT_LIMIT = 2
  23. class AWSPaginationTestCase(unittest.TestCase):
  24. def setUp(self):
  25. self.mock = mock_aws()
  26. self.mock.start()
  27. self.provider = CloudProviderFactory().create_provider(
  28. ProviderList.AWS,
  29. {'aws_access_key': 'a', 'aws_secret_key': 'b',
  30. 'aws_region_name': 'us-east-1',
  31. 'default_result_limit': RESULT_LIMIT})
  32. def tearDown(self):
  33. self.mock.stop()
  34. def _create_volumes(self):
  35. created = []
  36. for i in range(OBJECT_COUNT):
  37. vol = self.provider.storage.volumes.create(
  38. 'cb-page-%d' % i, 1)
  39. created.append(vol.id)
  40. return created
  41. def test_paging_visits_every_object_exactly_once(self):
  42. expected = self._create_volumes()
  43. seen = []
  44. page = self.provider.storage.volumes.list()
  45. seen.extend(o.id for o in page)
  46. pages = 1
  47. while page.is_truncated:
  48. page = self.provider.storage.volumes.list(marker=page.marker)
  49. seen.extend(o.id for o in page)
  50. pages += 1
  51. self.assertLess(pages, OBJECT_COUNT + 5,
  52. "Paging failed to terminate; the resume token is "
  53. "probably not advancing.")
  54. self.assertEqual(sorted(seen), sorted(expected),
  55. "Paging must visit every object exactly once.")
  56. self.assertEqual(len(seen), len(set(seen)),
  57. "Paging returned duplicates across pages.")
  58. def test_a_page_holds_no_more_than_the_result_limit(self):
  59. # The caller's limit bounds what comes back, independently of how
  60. # much was fetched per request to produce it.
  61. self._create_volumes()
  62. page = self.provider.storage.volumes.list()
  63. self.assertLessEqual(len(page), RESULT_LIMIT)
  64. def _page_size(self, list_op):
  65. # pylint:disable=protected-access
  66. return self.provider.storage.volumes.svc._page_size(
  67. self.provider.ec2_conn.meta.client, list_op)
  68. def test_transport_page_size_is_independent_of_the_result_limit(self):
  69. # The regression this guards: PageSize tracking the result limit is
  70. # what made a sparse scan pathological.
  71. page_size = self._page_size('describe_volumes')
  72. self.assertEqual(page_size, DEFAULT_PAGE_SIZE)
  73. self.assertNotEqual(page_size, RESULT_LIMIT)
  74. def test_page_size_is_clamped_to_the_operations_ceiling(self):
  75. # DescribeRouteTables allows 100 where most EC2 describes allow more,
  76. # and exceeding a ceiling is a hard InvalidParameterValue.
  77. self.assertEqual(self._page_size('describe_route_tables'), 100)
  78. def test_page_size_survives_an_unknown_operation(self):
  79. self.assertEqual(self._page_size('not_an_operation'),
  80. DEFAULT_PAGE_SIZE)
  81. def test_configured_page_size_is_used(self):
  82. self.provider.config['aws_page_size'] = 250
  83. self.assertEqual(self._page_size('describe_volumes'), 250)
  84. def test_configured_page_size_is_clamped_to_the_ceiling(self):
  85. # A configured value is no safer than the default: DescribeRouteTables
  86. # would reject anything above 100 outright.
  87. self.provider.config['aws_page_size'] = 900
  88. self.assertEqual(self._page_size('describe_route_tables'), 100)
  89. def test_configured_page_size_is_raised_to_the_floor(self):
  90. # Several EC2 describes require at least 5, so a smaller configured
  91. # value would be rejected rather than honoured.
  92. self.provider.config['aws_page_size'] = 1
  93. self.assertEqual(self._page_size('describe_vpcs'), 5)
  94. if __name__ == "__main__":
  95. unittest.main()