diff --git a/config.json b/config.json index 75ebd28bf80..0415e630d15 100644 --- a/config.json +++ b/config.json @@ -1482,6 +1482,18 @@ "loops" ] }, + { + "slug": "paasio", + "uuid": "ec36f9ed-f376-43d9-90ee-be0bea0cfc98", + "core": false, + "unlocked_by": "book-store", + "difficulty": 5, + "topics": [ + "classes", + "interfaces", + "networking" + ] + }, { "slug": "accumulate", "uuid": "e7351e8e-d3ff-4621-b818-cd55cf05bffd", diff --git a/exercises/paasio/README.md b/exercises/paasio/README.md new file mode 100644 index 00000000000..a031f124666 --- /dev/null +++ b/exercises/paasio/README.md @@ -0,0 +1,60 @@ +# PaaS I/O + +Report network IO statistics. + +You are writing a [PaaS][], and you need a way to bill customers based +on network and filesystem usage. + +Create a wrapper for network connections and files that can report IO +statistics. The wrapper must report: + +- The total number of bytes read/written. +- The total number of read/write operations. + +[PaaS]: http://en.wikipedia.org/wiki/Platform_as_a_service + +## Exception messages + +Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to +indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not +every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include +a message. + +To raise a message with an exception, just write it as an argument to the exception type. For example, instead of +`raise Exception`, you should write: + +```python +raise Exception("Meaningful message indicating the source of the error") +``` + +## Running the tests + +To run the tests, run `pytest paasio_test.py` + +Alternatively, you can tell Python to run the pytest module: +`python -m pytest paasio_test.py` + +### Common `pytest` options + +- `-v` : enable verbose output +- `-x` : stop running tests on first failure +- `--ff` : run failures from previous test before running other test cases + +For other options, see `python -m pytest -h` + +## Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/paasio` directory. + +You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`. + +For more detailed information about running tests, code style and linting, +please see [Running the Tests](http://exercism.io/tracks/python/tests). + +## Source + +Brian Matsuo [https://github.com/bmatsuo](https://github.com/bmatsuo) + +## Submitting Incomplete Solutions + +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/paasio/example.py b/exercises/paasio/example.py new file mode 100644 index 00000000000..54a404a68bd --- /dev/null +++ b/exercises/paasio/example.py @@ -0,0 +1,102 @@ +import io + + +class MeteredFile(io.BufferedRandom): + """Implement using a subclassing model.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._read_bytes = 0 + self._read_ops = 0 + self._write_bytes = 0 + self._write_ops = 0 + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return super().__exit__(exc_type, exc_val, exc_tb) + + def __iter__(self): + return self + + def __next__(self): + self._read_ops += 1 + data = super().readline() + self._read_bytes += len(data) + if data: + return data + raise StopIteration + + def read(self, size=-1): + self._read_ops += 1 + data = super().read(size) + self._read_bytes += len(data) + return data + + @property + def read_bytes(self): + return self._read_bytes + + @property + def read_ops(self): + return self._read_ops + + def write(self, b): + self._write_ops += 1 + length = super().write(b) + self._write_bytes += length + return length + + @property + def write_bytes(self): + return self._write_bytes + + @property + def write_ops(self): + return self._write_ops + + +class MeteredSocket: + """Implement using a delegation model.""" + + def __init__(self, socket): + self._socket = socket + self._recv_bytes = 0 + self._recv_ops = 0 + self._send_bytes = 0 + self._send_ops = 0 + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return self._socket.__exit__(exc_type, exc_val, exc_tb) + + def recv(self, bufsize, flags=0): + self._recv_ops += 1 + data = self._socket.recv(bufsize, flags) + self._recv_bytes += len(data) + return data + + @property + def recv_bytes(self): + return self._recv_bytes + + @property + def recv_ops(self): + return self._recv_ops + + def send(self, data, flags=0): + self._send_ops += 1 + length = self._socket.send(data, flags) + self._send_bytes += length + return length + + @property + def send_bytes(self): + return self._send_bytes + + @property + def send_ops(self): + return self._send_ops diff --git a/exercises/paasio/paasio.py b/exercises/paasio/paasio.py new file mode 100644 index 00000000000..62987c6a295 --- /dev/null +++ b/exercises/paasio/paasio.py @@ -0,0 +1,77 @@ +import io + + +class MeteredFile(io.BufferedRandom): + """Implement using a subclassing model.""" + + def __init__(self, *args, **kwargs): + pass + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def __iter__(self): + pass + + def __next__(self): + pass + + def read(self, size=-1): + pass + + @property + def read_bytes(self): + pass + + @property + def read_ops(self): + pass + + def write(self, b): + pass + + @property + def write_bytes(self): + pass + + @property + def write_ops(self): + pass + + +class MeteredSocket: + """Implement using a delegation model.""" + + def __init__(self, socket): + pass + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def recv(self, bufsize, flags=0): + pass + + @property + def recv_bytes(self): + pass + + @property + def recv_ops(self): + pass + + def send(self, data, flags=0): + pass + + @property + def send_bytes(self): + pass + + @property + def send_ops(self): + pass diff --git a/exercises/paasio/paasio_test.py b/exercises/paasio/paasio_test.py new file mode 100644 index 00000000000..9e3612b5192 --- /dev/null +++ b/exercises/paasio/paasio_test.py @@ -0,0 +1,884 @@ +import errno +import inspect +import io +import logging +import os +import unittest +from unittest.mock import ANY, call, NonCallableMagicMock, patch + +from paasio import MeteredFile, MeteredSocket + + +LOGGER = logging.getLogger(__name__) + +ZEN = b"""Beautiful is better than ugly. +Explicit is better than implicit. +Simple is better than complex. +Complex is better than complicated. +Flat is better than nested. +Sparse is better than dense. +Readability counts. +Special cases aren't special enough to break the rules. +Although practicality beats purity. +Errors should never pass silently. +Unless explicitly silenced. +In the face of ambiguity, refuse the temptation to guess. +There should be one-- and preferably only one --obvious way to do it. +Although that way may not be obvious at first unless you're Dutch. +Now is better than never. +Although never is often better than *right* now. +If the implementation is hard to explain, it's a bad idea. +If the implementation is easy to explain, it may be a good idea. +Namespaces are one honking great idea -- let's do more of those! +""" + + +# Implementation tests begin on line 456 +# Code below this line consists of mocks and tests for mocks compliance + + +class MockException(Exception): + pass + + +class MockFile(io.BytesIO): + def __init__(self, *args, chunk=None, exception=None, **kwargs): + super(MockFile, self).__init__(*args, **kwargs) + self.__chunk = chunk + self.__exception = exception + + def __exit__(self, exc_type, exc_val, exc_tb): + ret = super(MockFile, self).__exit__(exc_type, exc_val, exc_tb) + if exc_type is not None and "suppress" in exc_val.args[0]: + return True + return ret + + def read(self, size=-1): + if self.__exception is not None: + raise self.__exception + if self.__chunk is None: + return super(MockFile, self).read(size) + if size is None: + return super(MockFile, self).read(self.__chunk) + if size < 0: + return super(MockFile, self).read(self.__chunk) + return super(MockFile, self).read(min(self.__chunk, size)) + + def write(self, data): + if self.__chunk is None: + return super(MockFile, self).write(data) + return super(MockFile, self).write(data[: self.__chunk]) + + +class MockSock: + def __init__(self, *, chunk=None, exception=None): + self._recver = io.BytesIO(ZEN) + self._sender = io.BytesIO() + self.__closed = False + self.__chunk = chunk + self.__exception = exception + self.flags = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._recver.close() + self._sender.close() + self.__closed = True + if exc_type is not None and "suppress" in exc_val.args[0]: + return True + return False + + def recv(self, bufsize, flags=0): + if self.__closed: + raise OSError(errno.EBADF, os.strerror(errno.EBADF)) + if bufsize is None: + raise TypeError( + "'NoneType' object cannot be interpreted as an integer" + ) + if not isinstance(flags, int): + raise TypeError( + "an integer is required (got type {})".format( + type(flags).__name__ + ) + ) + self.flags = flags + if self.__exception is not None: + raise self.__exception + if self.__chunk is None: + return self._recver.read(bufsize) + else: + return self._recver.read(min(self.__chunk, bufsize)) + + def send(self, data, flags=0): + if self.__closed: + raise OSError(errno.EBADF, os.strerror(errno.EBADF)) + if not isinstance(flags, int): + raise TypeError( + "an integer is required (got type {})".format( + type(flags).__name__ + ) + ) + self.flags = flags + if self.__chunk is None: + return self._sender.write(data) + return self._sender.write(data[: self.__chunk]) + + +class SuperMock: + """Mock for super().__init__ calls only, as mock.MagicMock cannot.""" + + def __init__(self, *args, **kwargs): + if self.initialized: + self.init_called += 1 + else: + self.initialized = True + + def __call__(self, *args, **kwargs): + frame = inspect.currentframe() + if frame is None: + raise RuntimeError("Could not get current frame object") + stack = inspect.getouterframes(frame) + if any( + frame[3] == "__init__" and "paasio" in frame[1] for frame in stack + ): + return self + else: + return self.mock_object + + def __repr__(self): + return "".format( + hex(id(self)), self.mock_object + ) + + mock_object = None + init_called = 0 + initialized = False + + +class MockTestCase(unittest.TestCase): + def setUp(self): + self.test_passed = False + + def tearDown(self): + if not self.test_passed: + LOGGER.critical( + "\nError in mocks, please report to Exercism: %s", self.id() + ) + + def test_fixture_exception(self): + self.assertEqual(False, self.test_passed) + with self.assertLogs(__name__, level="CRITICAL") as logs: + self.tearDown() + self.assertEqual(1, len(logs.output)) + self.assertRegex(logs.output[0], ":\nError in mocks") + self.assertRegex(logs.output[0], "{}$".format(self.id())) + self.test_passed = True + + +class SuperMockTest(MockTestCase): + def test_super_mock_repr(self): + mock = SuperMock() + expected = "".format( + hex(id(mock)), repr(None) + ) + self.assertEqual(expected, repr(mock)) + file = MockFile() + mock.mock_object = file + expected = "".format( + hex(id(mock)), repr(file) + ) + self.assertEqual(expected, repr(mock)) + self.test_passed = True + + def test_super_mock_init(self): + self.assertIs(False, SuperMock.initialized) + mock = SuperMock() + self.assertIs(True, mock.initialized) + for calls in range(5): + self.assertEqual(calls, mock.init_called) + mock.__init__() + self.test_passed = True + + def test_super_mock_call(self): + def call2(obj): + return obj() + + def __init__(obj): + ret = call2(obj) + self.assertEqual(obj(), ret) + return ret + + mock = SuperMock() + self.assertIs(None, mock.mock_object) + wrapped = object() + mock.mock_object = wrapped + self.assertIs(wrapped, mock()) + self.assertIs(mock, __init__(mock)) + self.test_passed = True + + @patch("inspect.currentframe", return_value=None) + def test_super_mock_frame_error(self, inspect_mock): + with self.assertRaisesRegex( + RuntimeError, "^Could not get current frame object$" + ): + SuperMock()() + self.test_passed = True + + +class MockSockTest(MockTestCase): + def test_context_manager(self): + with MockSock() as socket: + socket.recv(30) + socket.send(b"data") + with self.assertRaisesRegex(OSError, os.strerror(errno.EBADF)): + socket.recv(30) + with self.assertRaisesRegex(OSError, os.strerror(errno.EBADF)): + socket.send(b"") + self.test_passed = True + + def test_context_manager_exception_raise(self): + exception = MockException("Should raise") + with self.assertRaisesRegex(MockException, "Should raise") as err: + with MockSock(exception=exception) as socket: + socket.recv(4096) + self.assertEqual(exception, err.exception) + self.test_passed = True + + def test_context_manager_exception_suppress(self): + exception = MockException("Should suppress") + with MockSock(exception=exception) as socket: + socket.recv(4096) + self.test_passed = True + + def test_recv_once(self): + with MockSock() as socket: + actual_recv = socket.recv(4096) + self.assertEqual(ZEN, actual_recv) + self.test_passed = True + + def test_recv_multiple(self): + actual_recv = b"" + with MockSock() as socket: + for _ in range(5): + actual_recv += socket.recv(30) + self.assertEqual(ZEN[:150], actual_recv) + self.test_passed = True + + def test_recv_multiple_chunk(self): + actual_recv = b"" + with MockSock(chunk=20) as socket: + for _ in range(5): + actual_recv += socket.recv(4096) + actual_recv += socket.recv(10) + self.assertEqual(ZEN[:110], actual_recv) + self.test_passed = True + + def test_recv_under_size(self): + with MockSock(chunk=257) as socket: + actual_recv = socket.recv(4096) + self.assertEqual(ZEN[:257], actual_recv) + self.test_passed = True + + def test_send_once(self): + with MockSock(chunk=257) as socket: + send_len = socket.send(ZEN) + self.assertEqual(ZEN[:257], socket._sender.getbuffer()) + self.assertEqual(257, send_len) + self.test_passed = True + + def test_send_multiple(self): + send_len = 0 + expected = b"Tomorrow's victory is today's practice." + with MockSock() as socket: + send_len += socket.send(b"Tomorro") + send_len += socket.send(b"w's victo") + send_len += socket.send(b"ry is today") + send_len += socket.send(b"'s practice.") + self.assertEqual(expected, socket._sender.getbuffer()) + self.assertEqual(39, send_len) + self.test_passed = True + + def test_send_under_size(self): + with MockSock(chunk=257) as socket: + send_len = socket.send(ZEN[:123]) + self.assertEqual(ZEN[:123], socket._sender.getbuffer()) + self.assertEqual(123, send_len) + self.test_passed = True + + def test_bufsize_required(self): + with self.assertRaisesRegex(TypeError, "argument"): + with MockSock() as socket: + socket.recv() + + with self.assertRaisesRegex(TypeError, "'NoneType'.+integer"): + with MockSock() as socket: + socket.recv(None) + self.test_passed = True + + def test_flags_support(self): + with MockSock() as socket: + self.assertEqual(len(ZEN), socket.send(ZEN, 42)) + self.assertEqual(ZEN, socket.recv(4096, 24)) + with MockSock() as mock_sock: + self.assertIs(None, mock_sock.flags) + mock_sock.recv(50) + self.assertEqual(0, mock_sock.flags) + mock_sock.send(b"no flags") + self.assertEqual(0, mock_sock.flags) + mock_sock.recv(30, 30) + self.assertEqual(30, mock_sock.flags) + mock_sock.send(b"flags", 1024) + self.assertEqual( + 1024, mock_sock.flags, + ) + with self.assertRaisesRegex( + TypeError, r"^an integer is.+NoneType\)$" + ): + mock_sock.send(b"data", None) + with self.assertRaisesRegex( + TypeError, r"^an integer is.+bytes\)$" + ): + mock_sock.send(b"data", b"flags") + with self.assertRaisesRegex( + TypeError, r"^an integer is.+NoneType\)$" + ): + mock_sock.recv(b"data", None) + with self.assertRaisesRegex( + TypeError, r"^an integer is.+bytes\)$" + ): + mock_sock.recv(b"data", b"flags") + self.test_passed = True + + +class MockFileTest(MockTestCase): + def test_context_manager(self): + with MockFile(ZEN) as file: + file.read() + with self.assertRaisesRegex( + ValueError, "I/O operation on closed file." + ): + file.read() + with self.assertRaisesRegex( + ValueError, "I/O operation on closed file." + ): + file.write(b"data") + self.test_passed = True + + def test_context_manager_exception_raise(self): + exception = MockException("Should raise") + mock = MockFile(ZEN, exception=exception) + with self.assertRaisesRegex(MockException, "Should raise") as err: + with mock as file: + file.read() + self.assertEqual(exception, err.exception) + self.test_passed = True + + def test_context_manager_exception_suppress(self): + exception = MockException("Should suppress") + mock = MockFile(ZEN, exception=exception) + with mock as file: + file.read() + self.test_passed = True + + def test_read_once(self): + with MockFile(ZEN) as file: + actual_read = file.read() + self.assertEqual(ZEN, actual_read) + + with MockFile(ZEN) as file: + actual_read = file.read(None) + self.assertEqual(ZEN, actual_read) + + with MockFile(ZEN) as file: + actual_read = file.read(-1) + self.assertEqual(ZEN, actual_read) + self.test_passed = True + + def test_read_multiple(self): + actual_read = b"" + with MockFile(ZEN) as file: + for _ in range(5): + actual_read += file.read(30) + self.assertEqual(ZEN[:150], actual_read) + self.test_passed = True + + def test_read_multiple_chunk(self): + actual_read = b"" + with MockFile(ZEN, chunk=20) as file: + for _ in range(5): + actual_read += file.read() + actual_read += file.read(10) + self.assertEqual(ZEN[:110], actual_read) + + actual_read = b"" + with MockFile(ZEN, chunk=20) as file: + for size in [None, -2, -1, 0, 1, 2]: + actual_read += file.read(size) + actual_read += file.read(10) + self.assertEqual(ZEN[:73], actual_read) + self.test_passed = True + + def test_read_under_size(self): + with MockFile(ZEN, chunk=257) as file: + actual_read = file.read() + self.assertEqual(ZEN[:257], actual_read) + self.test_passed = True + + def test_write_once(self): + with MockFile(chunk=257) as file: + write_len = file.write(ZEN) + self.assertEqual(ZEN[:257], file.getbuffer()) + self.assertEqual(257, write_len) + self.test_passed = True + + def test_write_multiple(self): + write_len = 0 + expected = b"Tomorrow's victory is today's practice." + with MockFile() as file: + write_len += file.write(b"Tomorro") + write_len += file.write(b"w's victo") + write_len += file.write(b"ry is today") + write_len += file.write(b"'s practice.") + self.assertEqual(expected, file.getbuffer()) + self.assertEqual(len(expected), write_len) + self.test_passed = True + + def test_write_under_size(self): + with MockFile(chunk=257) as file: + write_len = file.write(ZEN[:123]) + self.assertEqual(ZEN[:123], file.getbuffer()) + self.assertEqual(123, write_len) + self.test_passed = True + + +# Tests for paasio.py begin here + + +class MeteredSocketTest(unittest.TestCase): + def test_context_manager(self): + wrapped = MockSock() + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + mock.__exit__.side_effect = wrapped.__exit__ + with MeteredSocket(mock) as socket: + self.assertFalse(mock.__enter__.called) + socket.recv(30) + self.assertFalse(mock.__enter__.called) + mock.__exit__.assert_called_once_with(None, None, None) + self.assertEqual(2, len(mock.mock_calls)) + with self.assertRaisesRegex(OSError, os.strerror(errno.EBADF)): + socket.recv(30) + with self.assertRaisesRegex(OSError, os.strerror(errno.EBADF)): + socket.send(b"") + + def test_context_manager_exception_raise(self): + exception = MockException("Should raise") + wrapped = MockSock(exception=exception) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + mock.__exit__.side_effect = wrapped.__exit__ + with self.assertRaisesRegex(MockException, "Should raise") as err: + with MeteredSocket(mock) as socket: + self.assertFalse(mock.__enter__.called) + socket.recv(4096) + self.assertFalse(mock.__enter__.called) + mock.__exit__.assert_called_once_with( + MockException, err.exception, ANY, + ) + self.assertEqual(exception, err.exception) + + def test_context_manager_exception_suppress(self): + exception = MockException("Should suppress") + wrapped = MockSock(exception=exception) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + mock.__exit__.side_effect = wrapped.__exit__ + with MeteredSocket(mock) as socket: + self.assertFalse(mock.__enter__.called) + socket.recv(4096) + self.assertFalse(mock.__enter__.called) + mock.__exit__.assert_called_once_with( + MockException, exception, ANY, + ) + + def test_recv_once(self): + mock = NonCallableMagicMock(wraps=MockSock(), autospec=True) + with MeteredSocket(mock) as socket: + actual_recv = socket.recv(4096) + self.assertEqual(ZEN, actual_recv) + self.assertEqual(1, socket.recv_ops) + self.assertEqual(len(ZEN), socket.recv_bytes) + self.assertEqual(1, mock.recv.call_count) + + def test_recv_multiple(self): + wrapped = MockSock() + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + actual_recv = b"" + with MeteredSocket(mock) as socket: + for _ in range(5): + actual_recv += socket.recv(30) + self.assertEqual(ZEN[:150], actual_recv) + self.assertEqual(5, socket.recv_ops) + self.assertEqual(150, socket.recv_bytes) + self.assertEqual(5, mock.recv.call_count) + + def test_recv_multiple_chunk(self): + wrapped = MockSock(chunk=20) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + actual_recv = b"" + with MeteredSocket(mock) as socket: + for _ in range(5): + actual_recv += socket.recv(4096) + actual_recv += socket.recv(10) + self.assertEqual(ZEN[:110], actual_recv) + self.assertEqual(6, socket.recv_ops) + self.assertEqual(110, socket.recv_bytes) + self.assertEqual(6, mock.recv.call_count) + + def test_recv_under_size(self): + wrapped = MockSock(chunk=257) # largish odd number + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + with MeteredSocket(mock) as socket: + actual_recv = socket.recv(4096) + self.assertEqual(ZEN[:257], actual_recv) + self.assertEqual(1, socket.recv_ops) + self.assertEqual(257, socket.recv_bytes) + self.assertEqual(1, mock.recv.call_count) + + def test_send_once(self): + wrapped = MockSock(chunk=257) # largish odd number + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + with MeteredSocket(mock) as socket: + send_len = socket.send(ZEN) + self.assertEqual(ZEN[:257], wrapped._sender.getbuffer()) + self.assertEqual(257, send_len) + self.assertEqual(1, socket.send_ops) + self.assertEqual(257, socket.send_bytes) + self.assertEqual(1, mock.send.call_count) + + def test_send_multiple(self): + wrapped = MockSock() + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + send_len = 0 + expected = b"Tomorrow's victory is today's practice." + with MeteredSocket(mock) as socket: + send_len += socket.send(b"Tomorro") + send_len += socket.send(b"w's victo") + send_len += socket.send(b"ry is today") + send_len += socket.send(b"'s practice.") + self.assertEqual(expected, wrapped._sender.getbuffer()) + self.assertEqual(39, send_len) + self.assertEqual(4, socket.send_ops) + self.assertEqual(39, socket.send_bytes) + self.assertEqual(4, mock.send.call_count) + + def test_send_under_size(self): + wrapped = MockSock(chunk=257) # largish odd number + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + with MeteredSocket(mock) as socket: + send_len = socket.send(ZEN[:123]) + self.assertEqual(ZEN[:123], wrapped._sender.getbuffer()) + self.assertEqual(123, send_len) + self.assertEqual(1, socket.send_ops) + self.assertEqual(123, socket.send_bytes) + self.assertEqual(1, mock.send.call_count) + + def test_bufsize_required(self): + mock = NonCallableMagicMock(wraps=MockSock(), autospec=True) + with self.assertRaisesRegex(TypeError, "argument"): + with MeteredSocket(mock) as socket: + socket.recv() + self.assertFalse(mock.recv.called) + + mock = NonCallableMagicMock(wraps=MockSock(), autospec=True) + with self.assertRaisesRegex(TypeError, "^'NoneType'.+integer$"): + with MeteredSocket(mock) as socket: + socket.recv(None) + self.assertTrue( + call(None) in mock.recv.mock_calls + or call(None, ANY) in mock.recv.mock_calls + ) + + def test_flags_support(self): + mock = NonCallableMagicMock(wraps=MockSock(), autospec=True) + with MeteredSocket(mock) as socket: + self.assertEqual(len(ZEN), socket.send(ZEN, 42)) + self.assertEqual(ZEN, socket.recv(4096, 24)) + mock.send.assert_called_once_with(ZEN, 42) + mock.recv.assert_called_once_with(4096, 24) + + wrapped = MockSock() + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + with MeteredSocket(mock) as socket: + socket.recv(50) + self.assertEqual(0, wrapped.flags) + socket.send(b"no flags") + self.assertEqual(0, wrapped.flags) + socket.recv(30, 30) + self.assertEqual(30, wrapped.flags) + socket.send(b"flags", 1024) + self.assertEqual(1024, wrapped.flags) + with self.assertRaisesRegex(TypeError, "integer is required"): + socket.send(b"data", None) + with self.assertRaisesRegex(TypeError, "integer is required"): + socket.send(b"data", b"flags") + with self.assertRaisesRegex(TypeError, "integer is required"): + socket.recv(b"data", None) + with self.assertRaisesRegex(TypeError, "integer is required"): + socket.recv(b"data", b"flags") + + def test_stats_read_only(self): + mock = NonCallableMagicMock(wraps=MockSock(), autospec=True) + with MeteredSocket(mock) as socket: + self.assertEqual(0, socket.send_ops) + self.assertEqual(0, socket.send_bytes) + self.assertEqual(0, socket.recv_ops) + self.assertEqual(0, socket.recv_bytes) + for _ in range(277): + socket.send(b"b") + socket.send(b"bytes") + for _ in range(257): + socket.recv(1) + socket.recv(2) + self.assertEqual(278, socket.send_ops) + self.assertEqual(282, socket.send_bytes) + self.assertEqual(258, socket.recv_ops) + self.assertEqual(259, socket.recv_bytes) + with self.assertRaisesRegex(AttributeError, "can't set"): + socket.send_ops = 0 + with self.assertRaisesRegex(AttributeError, "can't set"): + socket.send_bytes = 0 + with self.assertRaisesRegex(AttributeError, "can't set"): + socket.recv_ops = 0 + with self.assertRaisesRegex(AttributeError, "can't set"): + socket.recv_bytes = 0 + self.assertEqual(278, socket.send_ops) + self.assertEqual(282, socket.send_bytes) + self.assertEqual(258, socket.recv_ops) + self.assertEqual(259, socket.recv_bytes) + + +@patch("paasio.super", create=True, new_callable=SuperMock) +class MeteredFileTest(unittest.TestCase): + def test_context_manager(self, super_mock): + wrapped = MockFile(ZEN) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + mock.__exit__.side_effect = wrapped.__exit__ + super_mock.mock_object = mock + with MeteredFile() as file: + self.assertEqual(1, super_mock.init_called) + self.assertFalse(mock.__enter__.called) + file.read() + self.assertFalse(mock.__enter__.called) + mock.__exit__.assert_called_once_with(None, None, None) + self.assertEqual(2, len(mock.mock_calls)) + with self.assertRaisesRegex( + ValueError, "I/O operation on closed file." + ): + file.read() + with self.assertRaisesRegex( + ValueError, "I/O operation on closed file." + ): + file.write(b"data") + + def test_context_manager_exception_raise(self, super_mock): + exception = MockException("Should raise") + wrapped = MockFile(ZEN, exception=exception) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + mock.__exit__.side_effect = wrapped.__exit__ + super_mock.mock_object = mock + with self.assertRaisesRegex(MockException, "Should raise") as err: + with MeteredFile() as file: + self.assertFalse(mock.__enter__.called) + file.read() + self.assertFalse(mock.__enter__.called) + mock.__exit__.assert_called_once_with( + MockException, err.exception, ANY, + ) + self.assertEqual(exception, err.exception) + + def test_context_manager_exception_suppress(self, super_mock): + exception = MockException("Should suppress") + wrapped = MockFile(ZEN, exception=exception) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + mock.__exit__.side_effect = wrapped.__exit__ + super_mock.mock_object = mock + with MeteredFile() as file: + self.assertFalse(mock.__enter__.called) + file.read() + self.assertFalse(mock.__enter__.called) + mock.__exit__.assert_called_once_with( + MockException, exception, ANY, + ) + + def test_iteration(self, super_mock): + mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True) + super_mock.mock_object = mock + actual_reads = b"" + file = MeteredFile() + for line in file: + actual_reads += line + self.assertLess( + 0, mock.readline.call_count, "File's readline not called" + ) + self.assertGreater( + 50, mock.readline.call_count, "Possible infinte loop detected" + ) + self.assertEqual(file.read_ops, mock.readline.call_count) + self.assertFalse(mock.__iter__.called) + self.assertEqual(len(ZEN), file.read_bytes) + self.assertEqual(ZEN, actual_reads) + + def test_read_once(self, super_mock): + mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True) + super_mock.mock_object = mock + with MeteredFile() as file: + actual_read = file.read() + self.assertEqual(ZEN, actual_read) + self.assertEqual((len(ZEN)), file.read_bytes) + self.assertEqual(1, file.read_ops) + self.assertEqual(mock.read.call_count, file.read_ops) + + mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True) + super_mock.mock_object = mock + with MeteredFile() as file: + actual_read = file.read(None) + self.assertEqual(ZEN, actual_read) + self.assertEqual((len(ZEN)), file.read_bytes) + self.assertEqual(1, file.read_ops) + self.assertEqual(mock.read.call_count, file.read_ops) + + mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True) + super_mock.mock_object = mock + with MeteredFile() as file: + actual_read = file.read(-1) + self.assertEqual(ZEN, actual_read) + self.assertEqual((len(ZEN)), file.read_bytes) + self.assertEqual(1, file.read_ops) + self.assertEqual(mock.read.call_count, file.read_ops) + + def test_read_multiple(self, super_mock): + wrapped = MockFile(ZEN) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + super_mock.mock_object = mock + actual_read = b"" + with MeteredFile() as file: + for _ in range(5): + actual_read += file.read(30) + self.assertEqual(ZEN[:150], actual_read) + self.assertEqual(5, file.read_ops) + self.assertEqual(150, file.read_bytes) + self.assertEqual(5, mock.read.call_count) + + def test_read_multiple_chunk(self, super_mock): + wrapped = MockFile(ZEN, chunk=20) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + super_mock.mock_object = mock + actual_read = b"" + with MeteredFile() as file: + for _ in range(5): + actual_read += file.read() + actual_read += file.read(10) + self.assertEqual(ZEN[:110], actual_read) + self.assertEqual(6, file.read_ops) + self.assertEqual(110, file.read_bytes) + self.assertEqual(6, mock.read.call_count) + + wrapped = MockFile(ZEN, chunk=20) + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + super_mock.mock_object = mock + actual_read = b"" + with MeteredFile() as file: + for size in [None, -2, -1, 0, 1, 2]: + actual_read += file.read(size) + actual_read += file.read(10) + self.assertEqual(ZEN[:73], actual_read) + self.assertEqual(7, file.read_ops) + self.assertEqual(73, file.read_bytes) + self.assertEqual(7, mock.read.call_count) + + def test_read_under_size(self, super_mock): + wrapped = MockFile(ZEN, chunk=257) # largish odd number + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + super_mock.mock_object = mock + with MeteredFile() as file: + actual_read = file.read() + self.assertEqual(ZEN[:257], actual_read) + self.assertEqual(1, file.read_ops) + self.assertEqual(257, file.read_bytes) + self.assertEqual(1, mock.read.call_count) + + def test_write_once(self, super_mock): + wrapped = MockFile(chunk=257) # largish odd number + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + super_mock.mock_object = mock + with MeteredFile() as file: + write_len = file.write(ZEN) + self.assertEqual(ZEN[:257], wrapped.getbuffer()) + self.assertEqual(257, write_len) + self.assertEqual(1, file.write_ops) + self.assertEqual(257, file.write_bytes) + self.assertEqual(1, mock.write.call_count) + + def test_write_multiple(self, super_mock): + wrapped = MockFile() + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + super_mock.mock_object = mock + write_len = 0 + expected = b"Tomorrow's victory is today's practice." + with MeteredFile() as file: + write_len += file.write(b"Tomorro") + write_len += file.write(b"w's victo") + write_len += file.write(b"ry is today") + write_len += file.write(b"'s practice.") + self.assertEqual(expected, wrapped.getbuffer()) + self.assertEqual(39, write_len) + self.assertEqual(4, file.write_ops) + self.assertEqual(39, file.write_bytes) + self.assertEqual(4, mock.write.call_count) + + def test_write_under_size(self, super_mock): + wrapped = MockFile(chunk=257) # largish odd number + mock = NonCallableMagicMock(wraps=wrapped, autospec=True) + super_mock.mock_object = mock + with MeteredFile() as file: + write_len = file.write(ZEN[:123]) + self.assertEqual(ZEN[:123], wrapped.getbuffer()) + self.assertEqual(123, write_len) + self.assertEqual(1, file.write_ops) + self.assertEqual(123, file.write_bytes) + self.assertEqual(1, mock.write.call_count) + + def test_stats_read_only(self, super_mock): + mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True) + super_mock.mock_object = mock + with MeteredFile() as file: + self.assertEqual(0, file.read_ops) + self.assertEqual(0, file.read_bytes) + for _ in range(57): + file.read(1) + file.read(2) + self.assertEqual(58, file.read_ops) + self.assertEqual(59, file.read_bytes) + self.assertEqual(0, file.write_ops) + self.assertEqual(0, file.write_bytes) + for _ in range(77): + file.write(b"b") + file.write(b"bytes") + self.assertEqual(78, file.write_ops) + self.assertEqual(82, file.write_bytes) + with self.assertRaisesRegex(AttributeError, "can't set"): + file.write_ops = 0 + with self.assertRaisesRegex(AttributeError, "can't set"): + file.write_bytes = 0 + with self.assertRaisesRegex(AttributeError, "can't set"): + file.read_ops = 0 + with self.assertRaisesRegex(AttributeError, "can't set"): + file.read_bytes = 0 + self.assertEqual(78, file.write_ops) + self.assertEqual(82, file.write_bytes) + self.assertEqual(58, file.read_ops) + self.assertEqual(59, file.read_bytes) + + +if __name__ == "__main__": + unittest.main()