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