| # | |
| # Licensed to the Apache Software Foundation (ASF) under one or more | |
| # contributor license agreements. See the NOTICE file distributed with | |
| # this work for additional information regarding copyright ownership. | |
| # The ASF licenses this file to You under the Apache License, Version 2.0 | |
| # (the "License"); you may not use this file except in compliance with | |
| # the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # | |
| """Tests for filesystemio.""" | |
| # pytype: skip-file | |
| import io | |
| import logging | |
| import multiprocessing | |
| import os | |
| import threading | |
| import unittest | |
| from apache_beam.io import filesystemio | |
| _LOGGER = logging.getLogger(__name__) | |
| class FakeDownloader(filesystemio.Downloader): | |
| def __init__(self, data): | |
| self._data = data | |
| self.last_read_size = -1 | |
| @property | |
| def size(self): | |
| return len(self._data) | |
| def get_range(self, start, end): | |
| self.last_read_size = end - start | |
| return self._data[start:end] | |
| class FakeUploader(filesystemio.Uploader): | |
| def __init__(self): | |
| self.data = b'' | |
| self.last_write_size = -1 | |
| self.finished = False | |
| def last_error(self): | |
| return None | |
| def put(self, data): | |
| assert not self.finished | |
| self.data += data.tobytes() | |
| self.last_write_size = len(data) | |
| def finish(self): | |
| self.finished = True | |
| class TestDownloaderStream(unittest.TestCase): | |
| def test_file_attributes(self): | |
| downloader = FakeDownloader(data=None) | |
| stream = filesystemio.DownloaderStream(downloader) | |
| self.assertEqual(stream.mode, 'rb') | |
| self.assertTrue(stream.readable()) | |
| self.assertFalse(stream.writable()) | |
| self.assertTrue(stream.seekable()) | |
| def test_read_empty(self): | |
| downloader = FakeDownloader(data=b'') | |
| stream = filesystemio.DownloaderStream(downloader) | |
| self.assertEqual(stream.read(), b'') | |
| def test_read(self): | |
| data = b'abcde' | |
| downloader = FakeDownloader(data) | |
| stream = filesystemio.DownloaderStream(downloader) | |
| # Read size is exactly what was passed to read() (unbuffered). | |
| self.assertEqual(stream.read(1), data[0:1]) | |
| self.assertEqual(downloader.last_read_size, 1) | |
| self.assertEqual(stream.read(), data[1:]) | |
| self.assertEqual(downloader.last_read_size, len(data) - 1) | |
| def test_read_buffered(self): | |
| data = b'abcde' | |
| downloader = FakeDownloader(data) | |
| buffer_size = 2 | |
| stream = io.BufferedReader( | |
| filesystemio.DownloaderStream(downloader), buffer_size) | |
| # Verify that buffering works and is reading ahead. | |
| self.assertEqual(stream.read(1), data[0:1]) | |
| self.assertEqual(downloader.last_read_size, buffer_size) | |
| self.assertEqual(stream.read(), data[1:]) | |
| class TestUploaderStream(unittest.TestCase): | |
| def test_file_attributes(self): | |
| uploader = FakeUploader() | |
| stream = filesystemio.UploaderStream(uploader) | |
| self.assertEqual(stream.mode, 'wb') | |
| self.assertFalse(stream.readable()) | |
| self.assertTrue(stream.writable()) | |
| self.assertFalse(stream.seekable()) | |
| def test_write_empty(self): | |
| uploader = FakeUploader() | |
| stream = filesystemio.UploaderStream(uploader) | |
| data = b'' | |
| stream.write(memoryview(data)) | |
| self.assertEqual(uploader.data, data) | |
| def test_write(self): | |
| data = b'abcde' | |
| uploader = FakeUploader() | |
| stream = filesystemio.UploaderStream(uploader) | |
| # Unbuffered writes. | |
| stream.write(memoryview(data[0:1])) | |
| self.assertEqual(uploader.data[0], data[0]) | |
| self.assertEqual(uploader.last_write_size, 1) | |
| stream.write(memoryview(data[1:])) | |
| self.assertEqual(uploader.data, data) | |
| self.assertEqual(uploader.last_write_size, len(data) - 1) | |
| def test_write_buffered(self): | |
| data = b'abcde' | |
| uploader = FakeUploader() | |
| buffer_size = 2 | |
| stream = io.BufferedWriter( | |
| filesystemio.UploaderStream(uploader), buffer_size) | |
| # Verify that buffering works: doesn't write to uploader until buffer is | |
| # filled. | |
| stream.write(data[0:1]) | |
| self.assertEqual(-1, uploader.last_write_size) | |
| stream.write(data[1:]) | |
| stream.close() | |
| self.assertEqual(data, uploader.data) | |
| class TestPipeStream(unittest.TestCase): | |
| def _read_and_verify(self, stream, expected, buffer_size, success): | |
| data_list = [] | |
| bytes_read = 0 | |
| seen_last_block = False | |
| while True: | |
| data = stream.read(buffer_size) | |
| self.assertLessEqual(len(data), buffer_size) | |
| if len(data) < buffer_size: | |
| # Test the constraint that the pipe stream returns less than the buffer | |
| # size only when at the end of the stream. | |
| if data: | |
| self.assertFalse(seen_last_block) | |
| seen_last_block = True | |
| if not data: | |
| break | |
| data_list.append(data) | |
| bytes_read += len(data) | |
| self.assertEqual(stream.tell(), bytes_read) | |
| self.assertEqual(b''.join(data_list), expected) | |
| success[0] = True | |
| def _read_and_seek(self, stream, expected, buffer_size, success): | |
| data_list = [] | |
| bytes_read = 0 | |
| while True: | |
| data = stream.read(buffer_size) | |
| # Test bad seek positions. | |
| with self.assertRaises(NotImplementedError): | |
| stream.seek(bytes_read + 1) | |
| with self.assertRaises(NotImplementedError): | |
| stream.seek(bytes_read - 1) | |
| # Rewind stream and test that it reads back the same data again. | |
| stream.seek(bytes_read) | |
| data2 = stream.read(buffer_size) | |
| self.assertEqual(data, data2) | |
| if not data: | |
| break | |
| data_list.append(data) | |
| bytes_read += len(data) | |
| self.assertEqual(stream.tell(), bytes_read) | |
| self.assertEqual(len(b''.join(data_list)), len(expected)) | |
| self.assertEqual(b''.join(data_list), expected) | |
| success[0] = True | |
| def test_pipe_stream(self): | |
| block_sizes = list(4**i for i in range(0, 12)) | |
| data_blocks = list(os.urandom(size) for size in block_sizes) | |
| expected = b''.join(data_blocks) | |
| buffer_sizes = [100001, 512 * 1024, 1024 * 1024] | |
| for buffer_size in buffer_sizes: | |
| for target in [self._read_and_verify, self._read_and_seek]: | |
| _LOGGER.info('buffer_size=%s, target=%s' % (buffer_size, target)) | |
| parent_conn, child_conn = multiprocessing.Pipe() | |
| stream = filesystemio.PipeStream(child_conn) | |
| success = [False] | |
| child_thread = threading.Thread( | |
| target=target, args=(stream, expected, buffer_size, success)) | |
| child_thread.start() | |
| for data in data_blocks: | |
| parent_conn.send_bytes(data) | |
| parent_conn.close() | |
| child_thread.join() | |
| self.assertTrue(success[0], 'error in test thread') | |
| def test_pipe_stream_rewind_buffer(self): | |
| buffer_size = 512 | |
| data = os.urandom(buffer_size) | |
| parent_conn, child_conn = multiprocessing.Pipe() | |
| parent_conn.send_bytes(data) | |
| parent_conn.close() | |
| stream = filesystemio.PipeStream(child_conn) | |
| # Regular read. | |
| read_data = stream.read(buffer_size) | |
| self.assertEqual(data, read_data) | |
| # Rewind buffer_size bytes. | |
| stream.seek(0) | |
| read_data = stream.read(buffer_size) | |
| self.assertEqual(data, read_data) | |
| # Read 0 bytes. Rewind buffer still points to offset 0. | |
| read_data = stream.read(buffer_size) | |
| self.assertFalse(read_data) | |
| stream.seek(0) | |
| read_data = stream.read(buffer_size) | |
| self.assertEqual(data, read_data) | |
| if __name__ == '__main__': | |
| logging.getLogger().setLevel(logging.INFO) | |
| unittest.main() |