github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/python/tests/botocore_common.py (about)

     1  #
     2  # Copyright (c) 2018-2022, NVIDIA CORPORATION. All rights reserved.
     3  #
     4  
     5  # pylint: disable=missing-module-docstring
     6  import io
     7  import logging
     8  import unittest
     9  
    10  import boto3
    11  from moto import mock_s3
    12  from botocore.exceptions import ClientError
    13  
    14  from aistore.sdk.const import UTF_ENCODING
    15  from tests import (
    16      AWS_ACCESS_KEY_ID,
    17      AWS_SECRET_ACCESS_KEY,
    18      AWS_SESSION_TOKEN,
    19      AWS_REGION,
    20  )
    21  from tests.utils import random_string
    22  from tests.unit.botocore_patch import mock_s3_redirect
    23  
    24  
    25  # pylint: disable=too-many-instance-attributes,missing-function-docstring,invalid-name,unused-variable
    26  class BotocoreBaseTest(unittest.TestCase):
    27      """
    28      Common test group for the botocore monkey patch;
    29      Runs a small set of S3 operations.
    30  
    31      We run this over and over, varying whether redirects
    32      are issued, and whether our monkey patch is loaded
    33      to handle them.
    34  
    35      If botocore has been monkeypatched, it should
    36      not get upset when redirected.
    37  
    38      If botocore has not, it should get upset every time.
    39  
    40      For units, we use moto to mock an S3 instance; to
    41      control whether redirects are issued we use a
    42      decorator (see mock_s3_redirect.py).
    43  
    44      To control botocore's expected behavior we use the
    45      redirect_errors_expected property.
    46      """
    47  
    48      __test__ = False
    49      mock_s3 = mock_s3()
    50  
    51      def __init__(self, *args, **kwargs):
    52          super().__init__(*args, **kwargs)
    53          self.enable_redirects = False
    54          self.redirect_errors_expected = False
    55  
    56          # Use moto to mock S3 by default.
    57          self.use_moto = True
    58          # AIstore endpoint URL to use iff we're not using moto.
    59          self.endpoint_url = kwargs.get("endpoint_url", "http://localhost:8080/s3")
    60  
    61      def setUp(self):
    62          self.control_bucket = random_string()
    63          self.control_object = random_string()
    64          self.another_bucket = random_string()
    65          self.another_object = random_string()
    66  
    67          if self.use_moto:
    68              logging.debug("Using moto for S3 services")
    69              # Disable any redirections until we're ready.
    70              mock_s3_redirect.redirections_enabled = False
    71              self.mock_s3.start()
    72              self.s3 = boto3.client(
    73                  "s3", region_name=AWS_REGION
    74              )  # pylint: disable=invalid-name
    75          else:
    76              logging.debug("Using aistore for S3 services")
    77              self.s3 = boto3.client(
    78                  "s3",
    79                  region_name=AWS_REGION,
    80                  endpoint_url=self.endpoint_url,
    81                  aws_access_key_id=AWS_ACCESS_KEY_ID,
    82                  aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
    83                  aws_session_token=AWS_SESSION_TOKEN,
    84              )
    85  
    86          self.s3.create_bucket(Bucket=self.control_bucket)
    87          self.s3.upload_fileobj(
    88              io.BytesIO(b"Hello, world!"), self.control_bucket, self.control_object
    89          )
    90  
    91          if self.use_moto:
    92              # Enable redirections if we've been asked to do so.
    93              mock_s3_redirect.redirections_enabled = self.enable_redirects
    94  
    95      def tearDown(self):
    96          if self.use_moto:
    97              self.mock_s3.stop()
    98          else:
    99              try:
   100                  self.s3.delete_object(
   101                      Bucket=self.control_bucket, Key=self.control_object
   102                  )
   103              except Exception:
   104                  pass
   105              try:
   106                  self.s3.delete_bucket(Bucket=self.control_bucket)
   107              except Exception:
   108                  pass
   109              try:
   110                  self.s3.delete_object(
   111                      Bucket=self.control_bucket, Key=self.another_object
   112                  )
   113              except Exception:
   114                  pass
   115              try:
   116                  self.s3.delete_bucket(Bucket=self.another_bucket)
   117              except Exception:
   118                  pass
   119  
   120      def test_bucket_create(self):
   121          # When integration testing against a real aistore, this won't redirect.
   122          redirect_errors_expected = (
   123              False if not self.use_moto else self.redirect_errors_expected
   124          )
   125  
   126          with MightRedirect(redirect_errors_expected, operation="_bucket_response_put"):
   127              logging.warning("Creating bucket %s", self.another_bucket)
   128              self.s3.create_bucket(Bucket=self.another_bucket)
   129  
   130      def test_bucket_list(self):
   131          # Our redirect mock can't intercept bucket listing operations;
   132          # so, always expect success
   133          self.assertIn(
   134              self.control_bucket, [b["Name"] for b in self.s3.list_buckets()["Buckets"]]
   135          )
   136  
   137      def test_object_create(self):
   138          with MightRedirect(self.redirect_errors_expected):
   139              self.s3.upload_fileobj(
   140                  io.BytesIO(b"Hello, world!"), self.control_bucket, self.another_object
   141              )
   142  
   143      def test_object_list(self):
   144          # Our redirect mock can't intercept object listing operations;
   145          # so, always expect success
   146          self.assertEqual(
   147              [
   148                  b["Key"]
   149                  for b in self.s3.list_objects(Bucket=self.control_bucket)["Contents"]
   150              ],
   151              [self.control_object],
   152          )
   153  
   154      def test_object_get(self):
   155          with MightRedirect(self.redirect_errors_expected):
   156              stream_str = io.BytesIO()
   157              self.s3.download_fileobj(
   158                  self.control_bucket, self.control_object, stream_str
   159              )
   160              self.assertEqual(
   161                  stream_str.getvalue().decode(UTF_ENCODING), "Hello, world!"
   162              )
   163  
   164      def test_caching(self):
   165          with MightRedirect(self.redirect_errors_expected):
   166              stream_str = io.BytesIO()
   167              self.s3.download_fileobj(
   168                  self.control_bucket, self.control_object, stream_str
   169              )
   170              self.assertEqual(
   171                  stream_str.getvalue().decode(UTF_ENCODING), "Hello, world!"
   172              )
   173  
   174              self.s3.download_fileobj(
   175                  self.control_bucket, self.control_object, stream_str
   176              )
   177              self.assertEqual(
   178                  stream_str.getvalue().decode(UTF_ENCODING), "Hello, world!"
   179              )
   180  
   181      def test_object_delete(self):
   182          with MightRedirect(self.redirect_errors_expected):
   183              self.s3.delete_object(Bucket=self.control_bucket, Key=self.control_object)
   184  
   185      def test_bucket_delete(self):
   186          with MightRedirect(self.redirect_errors_expected):
   187              self.s3.delete_object(Bucket=self.control_bucket, Key=self.control_object)
   188              self.s3.delete_bucket(Bucket=self.control_bucket)
   189  
   190  
   191  class MightRedirect:
   192      """
   193      Context manager to handle botocore errors.
   194  
   195      Some test sets expect botocore to issue errors
   196      when it encounters redirects. Others expect
   197      the opposite.
   198  
   199      This allows us to control the expected behavior.
   200      """
   201  
   202      max_retries = 3
   203  
   204      def __init__(self, redirect_errors_expected=False, operation=None):
   205          self.redirect_errors_expected = redirect_errors_expected
   206          self.operation = operation
   207  
   208      def __enter__(self):
   209          return self
   210  
   211      def __exit__(self, exc, value, traceback):
   212          if self.redirect_errors_expected:
   213              try:
   214                  if exc and value:
   215                      raise value
   216              except ClientError as ex:
   217                  # Some operations don't pass through redirect errors directly
   218                  if self.operation in ["_bucket_response_put"]:
   219                      return True
   220                  if int(ex.response["Error"]["Code"]) in [302, 307]:
   221                      return True
   222              instead = "No error"
   223              if value:
   224                  instead = value
   225  
   226              raise Exception(
   227                  "A ClientError with a redirect code was expected, "
   228                  + "but didn't happen. Instead: "
   229                  + instead
   230              )
   231          return False