__init__.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. # Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the 'License'). You
  4. # may not use this file except in compliance with the License. A copy of
  5. # the License is located at
  6. #
  7. # http://aws.amazon.com/apache2.0/
  8. #
  9. # or in the 'license' file accompanying this file. This file is
  10. # distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  11. # ANY KIND, either express or implied. See the License for the specific
  12. # language governing permissions and limitations under the License.
  13. import hashlib
  14. import io
  15. import math
  16. import os
  17. import platform
  18. import shutil
  19. import string
  20. import tempfile
  21. import unittest
  22. from unittest import mock # noqa: F401
  23. import botocore.session
  24. from botocore.stub import Stubber
  25. from s3transfer.futures import (
  26. IN_MEMORY_DOWNLOAD_TAG,
  27. IN_MEMORY_UPLOAD_TAG,
  28. BoundedExecutor,
  29. NonThreadedExecutor,
  30. TransferCoordinator,
  31. TransferFuture,
  32. TransferMeta,
  33. )
  34. from s3transfer.manager import TransferConfig
  35. from s3transfer.subscribers import BaseSubscriber
  36. from s3transfer.utils import (
  37. CallArgs,
  38. OSUtils,
  39. SlidingWindowSemaphore,
  40. TaskSemaphore,
  41. )
  42. ORIGINAL_EXECUTOR_CLS = BoundedExecutor.EXECUTOR_CLS
  43. # Detect if CRT is available for use
  44. try:
  45. import awscrt.s3 # noqa: F401
  46. HAS_CRT = True
  47. except ImportError:
  48. HAS_CRT = False
  49. def setup_package():
  50. if is_serial_implementation():
  51. BoundedExecutor.EXECUTOR_CLS = NonThreadedExecutor
  52. def teardown_package():
  53. BoundedExecutor.EXECUTOR_CLS = ORIGINAL_EXECUTOR_CLS
  54. def is_serial_implementation():
  55. return os.environ.get('USE_SERIAL_EXECUTOR', False)
  56. def assert_files_equal(first, second):
  57. if os.path.getsize(first) != os.path.getsize(second):
  58. raise AssertionError(f"Files are not equal: {first}, {second}")
  59. first_md5 = md5_checksum(first)
  60. second_md5 = md5_checksum(second)
  61. if first_md5 != second_md5:
  62. raise AssertionError(
  63. "Files are not equal: {}(md5={}) != {}(md5={})".format(
  64. first, first_md5, second, second_md5
  65. )
  66. )
  67. def md5_checksum(filename):
  68. checksum = hashlib.md5()
  69. with open(filename, 'rb') as f:
  70. for chunk in iter(lambda: f.read(8192), b''):
  71. checksum.update(chunk)
  72. return checksum.hexdigest()
  73. def random_bucket_name(prefix='s3transfer', num_chars=10):
  74. base = string.ascii_lowercase + string.digits
  75. random_bytes = bytearray(os.urandom(num_chars))
  76. return prefix + ''.join([base[b % len(base)] for b in random_bytes])
  77. def skip_if_windows(reason):
  78. """Decorator to skip tests that should not be run on windows.
  79. Example usage:
  80. @skip_if_windows("Not valid")
  81. def test_some_non_windows_stuff(self):
  82. self.assertEqual(...)
  83. """
  84. def decorator(func):
  85. return unittest.skipIf(
  86. platform.system() not in ['Darwin', 'Linux'], reason
  87. )(func)
  88. return decorator
  89. def skip_if_using_serial_implementation(reason):
  90. """Decorator to skip tests when running as the serial implementation"""
  91. def decorator(func):
  92. return unittest.skipIf(is_serial_implementation(), reason)(func)
  93. return decorator
  94. def requires_crt(cls, reason=None):
  95. if reason is None:
  96. reason = "Test requires awscrt to be installed."
  97. return unittest.skipIf(not HAS_CRT, reason)(cls)
  98. class StreamWithError:
  99. """A wrapper to simulate errors while reading from a stream
  100. :param stream: The underlying stream to read from
  101. :param exception_type: The exception type to throw
  102. :param num_reads: The number of times to allow a read before raising
  103. the exception. A value of zero indicates to raise the error on the
  104. first read.
  105. """
  106. def __init__(self, stream, exception_type, num_reads=0):
  107. self._stream = stream
  108. self._exception_type = exception_type
  109. self._num_reads = num_reads
  110. self._count = 0
  111. def read(self, n=-1):
  112. if self._count == self._num_reads:
  113. raise self._exception_type
  114. self._count += 1
  115. return self._stream.read(n)
  116. class FileSizeProvider:
  117. def __init__(self, file_size):
  118. self.file_size = file_size
  119. def on_queued(self, future, **kwargs):
  120. future.meta.provide_transfer_size(self.file_size)
  121. class FileCreator:
  122. def __init__(self):
  123. self.rootdir = tempfile.mkdtemp()
  124. def remove_all(self):
  125. shutil.rmtree(self.rootdir)
  126. def create_file(self, filename, contents, mode='w'):
  127. """Creates a file in a tmpdir
  128. ``filename`` should be a relative path, e.g. "foo/bar/baz.txt"
  129. It will be translated into a full path in a tmp dir.
  130. ``mode`` is the mode the file should be opened either as ``w`` or
  131. `wb``.
  132. Returns the full path to the file.
  133. """
  134. full_path = os.path.join(self.rootdir, filename)
  135. if not os.path.isdir(os.path.dirname(full_path)):
  136. os.makedirs(os.path.dirname(full_path))
  137. with open(full_path, mode) as f:
  138. f.write(contents)
  139. return full_path
  140. def create_file_with_size(self, filename, filesize):
  141. filename = self.create_file(filename, contents='')
  142. chunksize = 8192
  143. with open(filename, 'wb') as f:
  144. for i in range(int(math.ceil(filesize / float(chunksize)))):
  145. f.write(b'a' * chunksize)
  146. return filename
  147. def append_file(self, filename, contents):
  148. """Append contents to a file
  149. ``filename`` should be a relative path, e.g. "foo/bar/baz.txt"
  150. It will be translated into a full path in a tmp dir.
  151. Returns the full path to the file.
  152. """
  153. full_path = os.path.join(self.rootdir, filename)
  154. if not os.path.isdir(os.path.dirname(full_path)):
  155. os.makedirs(os.path.dirname(full_path))
  156. with open(full_path, 'a') as f:
  157. f.write(contents)
  158. return full_path
  159. def full_path(self, filename):
  160. """Translate relative path to full path in temp dir.
  161. f.full_path('foo/bar.txt') -> /tmp/asdfasd/foo/bar.txt
  162. """
  163. return os.path.join(self.rootdir, filename)
  164. class RecordingOSUtils(OSUtils):
  165. """An OSUtil abstraction that records openings and renamings"""
  166. def __init__(self):
  167. super().__init__()
  168. self.open_records = []
  169. self.rename_records = []
  170. def open(self, filename, mode):
  171. self.open_records.append((filename, mode))
  172. return super().open(filename, mode)
  173. def rename_file(self, current_filename, new_filename):
  174. self.rename_records.append((current_filename, new_filename))
  175. super().rename_file(current_filename, new_filename)
  176. class RecordingSubscriber(BaseSubscriber):
  177. def __init__(self):
  178. self.on_queued_calls = []
  179. self.on_progress_calls = []
  180. self.on_done_calls = []
  181. def on_queued(self, **kwargs):
  182. self.on_queued_calls.append(kwargs)
  183. def on_progress(self, **kwargs):
  184. self.on_progress_calls.append(kwargs)
  185. def on_done(self, **kwargs):
  186. self.on_done_calls.append(kwargs)
  187. def calculate_bytes_seen(self, **kwargs):
  188. amount_seen = 0
  189. for call in self.on_progress_calls:
  190. amount_seen += call['bytes_transferred']
  191. return amount_seen
  192. class TransferCoordinatorWithInterrupt(TransferCoordinator):
  193. """Used to inject keyboard interrupts"""
  194. def result(self):
  195. raise KeyboardInterrupt()
  196. class RecordingExecutor:
  197. """A wrapper on an executor to record calls made to submit()
  198. You can access the submissions property to receive a list of dictionaries
  199. that represents all submissions where the dictionary is formatted::
  200. {
  201. 'fn': function
  202. 'args': positional args (as tuple)
  203. 'kwargs': keyword args (as dict)
  204. }
  205. """
  206. def __init__(self, executor):
  207. self._executor = executor
  208. self.submissions = []
  209. def submit(self, task, tag=None, block=True):
  210. future = self._executor.submit(task, tag, block)
  211. self.submissions.append({'task': task, 'tag': tag, 'block': block})
  212. return future
  213. def shutdown(self):
  214. self._executor.shutdown()
  215. class StubbedClientTest(unittest.TestCase):
  216. def setUp(self):
  217. self.session = botocore.session.get_session()
  218. self.region = 'us-west-2'
  219. self.client = self.session.create_client(
  220. 's3',
  221. self.region,
  222. aws_access_key_id='foo',
  223. aws_secret_access_key='bar',
  224. )
  225. self.stubber = Stubber(self.client)
  226. self.stubber.activate()
  227. def tearDown(self):
  228. self.stubber.deactivate()
  229. def reset_stubber_with_new_client(self, override_client_kwargs):
  230. client_kwargs = {
  231. 'service_name': 's3',
  232. 'region_name': self.region,
  233. 'aws_access_key_id': 'foo',
  234. 'aws_secret_access_key': 'bar',
  235. }
  236. client_kwargs.update(override_client_kwargs)
  237. self.client = self.session.create_client(**client_kwargs)
  238. self.stubber = Stubber(self.client)
  239. self.stubber.activate()
  240. class BaseTaskTest(StubbedClientTest):
  241. def setUp(self):
  242. super().setUp()
  243. self.transfer_coordinator = TransferCoordinator()
  244. def get_task(self, task_cls, **kwargs):
  245. if 'transfer_coordinator' not in kwargs:
  246. kwargs['transfer_coordinator'] = self.transfer_coordinator
  247. return task_cls(**kwargs)
  248. def get_transfer_future(self, call_args=None):
  249. return TransferFuture(
  250. meta=TransferMeta(call_args), coordinator=self.transfer_coordinator
  251. )
  252. class BaseSubmissionTaskTest(BaseTaskTest):
  253. def setUp(self):
  254. super().setUp()
  255. self.config = TransferConfig()
  256. self.osutil = OSUtils()
  257. self.executor = BoundedExecutor(
  258. 1000,
  259. 1,
  260. {
  261. IN_MEMORY_UPLOAD_TAG: TaskSemaphore(10),
  262. IN_MEMORY_DOWNLOAD_TAG: SlidingWindowSemaphore(10),
  263. },
  264. )
  265. def tearDown(self):
  266. super().tearDown()
  267. self.executor.shutdown()
  268. class BaseGeneralInterfaceTest(StubbedClientTest):
  269. """A general test class to ensure consistency across TransferManger methods
  270. This test should never be called and should be subclassed from to pick up
  271. the various tests that all TransferManager method must pass from a
  272. functionality standpoint.
  273. """
  274. __test__ = False
  275. def manager(self):
  276. """The transfer manager to use"""
  277. raise NotImplementedError('method is not implemented')
  278. @property
  279. def method(self):
  280. """The transfer manager method to invoke i.e. upload()"""
  281. raise NotImplementedError('method is not implemented')
  282. def create_call_kwargs(self):
  283. """The kwargs to be passed to the transfer manager method"""
  284. raise NotImplementedError('create_call_kwargs is not implemented')
  285. def create_invalid_extra_args(self):
  286. """A value for extra_args that will cause validation errors"""
  287. raise NotImplementedError(
  288. 'create_invalid_extra_args is not implemented'
  289. )
  290. def create_stubbed_responses(self):
  291. """A list of stubbed responses that will cause the request to succeed
  292. The elements of this list is a dictionary that will be used as key
  293. word arguments to botocore.Stubber.add_response(). For example::
  294. [{'method': 'put_object', 'service_response': {}}]
  295. """
  296. raise NotImplementedError(
  297. 'create_stubbed_responses is not implemented'
  298. )
  299. def create_expected_progress_callback_info(self):
  300. """A list of kwargs expected to be passed to each progress callback
  301. Note that the future kwargs does not need to be added to each
  302. dictionary provided in the list. This is injected for you. An example
  303. is::
  304. [
  305. {'bytes_transferred': 4},
  306. {'bytes_transferred': 4},
  307. {'bytes_transferred': 2}
  308. ]
  309. This indicates that the progress callback will be called three
  310. times and pass along the specified keyword arguments and corresponding
  311. values.
  312. """
  313. raise NotImplementedError(
  314. 'create_expected_progress_callback_info is not implemented'
  315. )
  316. def _setup_default_stubbed_responses(self):
  317. for stubbed_response in self.create_stubbed_responses():
  318. self.stubber.add_response(**stubbed_response)
  319. def test_returns_future_with_meta(self):
  320. self._setup_default_stubbed_responses()
  321. future = self.method(**self.create_call_kwargs())
  322. # The result is called so we ensure that the entire process executes
  323. # before we try to clean up resources in the tearDown.
  324. future.result()
  325. # Assert the return value is a future with metadata associated to it.
  326. self.assertIsInstance(future, TransferFuture)
  327. self.assertIsInstance(future.meta, TransferMeta)
  328. def test_returns_correct_call_args(self):
  329. self._setup_default_stubbed_responses()
  330. call_kwargs = self.create_call_kwargs()
  331. future = self.method(**call_kwargs)
  332. # The result is called so we ensure that the entire process executes
  333. # before we try to clean up resources in the tearDown.
  334. future.result()
  335. # Assert that there are call args associated to the metadata
  336. self.assertIsInstance(future.meta.call_args, CallArgs)
  337. # Assert that all of the arguments passed to the method exist and
  338. # are of the correct value in call_args.
  339. for param, value in call_kwargs.items():
  340. self.assertEqual(value, getattr(future.meta.call_args, param))
  341. def test_has_transfer_id_associated_to_future(self):
  342. self._setup_default_stubbed_responses()
  343. call_kwargs = self.create_call_kwargs()
  344. future = self.method(**call_kwargs)
  345. # The result is called so we ensure that the entire process executes
  346. # before we try to clean up resources in the tearDown.
  347. future.result()
  348. # Assert that an transfer id was associated to the future.
  349. # Since there is only one transfer request is made for that transfer
  350. # manager the id will be zero since it will be the first transfer
  351. # request made for that transfer manager.
  352. self.assertEqual(future.meta.transfer_id, 0)
  353. # If we make a second request, the transfer id should have incremented
  354. # by one for that new TransferFuture.
  355. self._setup_default_stubbed_responses()
  356. future = self.method(**call_kwargs)
  357. future.result()
  358. self.assertEqual(future.meta.transfer_id, 1)
  359. def test_invalid_extra_args(self):
  360. with self.assertRaisesRegex(ValueError, 'Invalid extra_args'):
  361. self.method(
  362. extra_args=self.create_invalid_extra_args(),
  363. **self.create_call_kwargs(),
  364. )
  365. def test_for_callback_kwargs_correctness(self):
  366. # Add the stubbed responses before invoking the method
  367. self._setup_default_stubbed_responses()
  368. subscriber = RecordingSubscriber()
  369. future = self.method(
  370. subscribers=[subscriber], **self.create_call_kwargs()
  371. )
  372. # We call shutdown instead of result on future because the future
  373. # could be finished but the done callback could still be going.
  374. # The manager's shutdown method ensures everything completes.
  375. self.manager.shutdown()
  376. # Assert the various subscribers were called with the
  377. # expected kwargs
  378. expected_progress_calls = self.create_expected_progress_callback_info()
  379. for expected_progress_call in expected_progress_calls:
  380. expected_progress_call['future'] = future
  381. self.assertEqual(subscriber.on_queued_calls, [{'future': future}])
  382. self.assertEqual(subscriber.on_progress_calls, expected_progress_calls)
  383. self.assertEqual(subscriber.on_done_calls, [{'future': future}])
  384. class NonSeekableReader(io.RawIOBase):
  385. def __init__(self, b=b''):
  386. super().__init__()
  387. self._data = io.BytesIO(b)
  388. def seekable(self):
  389. return False
  390. def writable(self):
  391. return False
  392. def readable(self):
  393. return True
  394. def write(self, b):
  395. # This is needed because python will not always return the correct
  396. # kind of error even though writeable returns False.
  397. raise io.UnsupportedOperation("write")
  398. def read(self, n=-1):
  399. return self._data.read(n)
  400. class NonSeekableWriter(io.RawIOBase):
  401. def __init__(self, fileobj):
  402. super().__init__()
  403. self._fileobj = fileobj
  404. def seekable(self):
  405. return False
  406. def writable(self):
  407. return True
  408. def readable(self):
  409. return False
  410. def write(self, b):
  411. self._fileobj.write(b)
  412. def read(self, n=-1):
  413. raise io.UnsupportedOperation("read")