From c849e3c6ef913ff889a93c2542ea4f35726f7a64 Mon Sep 17 00:00:00 2001 From: Dobrianskyi Nikita <34073648+ndobryanskyy@users.noreply.github.com> Date: Fri, 21 Dec 2018 20:11:54 +0200 Subject: [PATCH] fix: Flush stdout and stderr on each write when debugging (#843) * Introduce StreamWriter to wrap output streams. Fix #835 * Fix typo --- .../local/cli_common/invoke_context.py | 31 +-- samcli/commands/local/lib/debug_context.py | 3 - samcli/commands/local/lib/local_lambda.py | 20 +- samcli/lib/utils/stream_writer.py | 37 ++++ samcli/local/apigw/local_apigw_service.py | 41 ++-- samcli/local/docker/container.py | 15 +- samcli/local/docker/manager.py | 29 ++- .../local_lambda_invoke_service.py | 5 +- .../commands/local/lib/test_local_lambda.py | 8 +- .../local/docker/test_lambda_container.py | 7 +- .../functional/local/lambdafn/test_runtime.py | 22 +- .../local/cli_common/test_invoke_context.py | 203 ++++++++++++++++-- .../commands/local/lib/test_debug_context.py | 3 - tests/unit/lib/utils/test_stream_writer.py | 52 +++++ 14 files changed, 396 insertions(+), 80 deletions(-) create mode 100644 samcli/lib/utils/stream_writer.py create mode 100644 tests/unit/lib/utils/test_stream_writer.py diff --git a/samcli/commands/local/cli_common/invoke_context.py b/samcli/commands/local/cli_common/invoke_context.py index 56ec5f1282f6..9e6bc99371b0 100644 --- a/samcli/commands/local/cli_common/invoke_context.py +++ b/samcli/commands/local/cli_common/invoke_context.py @@ -7,6 +7,7 @@ import os import samcli.lib.utils.osutils as osutils +from samcli.lib.utils.stream_writer import StreamWriter from samcli.commands.local.lib.local_lambda import LocalLambdaRunner from samcli.commands.local.lib.debug_context import DebugContext from samcli.local.lambdafn.runtime import LambdaRuntime @@ -202,26 +203,28 @@ def local_lambda_runner(self): @property def stdout(self): """ - Returns a stdout stream to output Lambda function logs to + Returns stream writer for stdout to output Lambda function logs to - :return File like object: Stream where the output of the function is sent to + Returns + ------- + samcli.lib.utils.stream_writer.StreamWriter + Stream writer for stdout """ - if self._log_file_handle: - return self._log_file_handle - - return osutils.stdout() + stream = self._log_file_handle if self._log_file_handle else osutils.stdout() + return StreamWriter(stream, self._is_debugging) @property def stderr(self): """ - Returns stderr stream to output Lambda function errors to + Returns stream writer for stderr to output Lambda function errors to - :return File like object: Stream where the stderr of the function is sent to + Returns + ------- + samcli.lib.utils.stream_writer.StreamWriter + Stream writer for stderr """ - if self._log_file_handle: - return self._log_file_handle - - return osutils.stderr() + stream = self._log_file_handle if self._log_file_handle else osutils.stderr() + return StreamWriter(stream, self._is_debugging) @property def template(self): @@ -256,6 +259,10 @@ def parameter_overrides(self): return self._parameter_overrides + @property + def _is_debugging(self): + return bool(self._debug_context) + @staticmethod def _get_template_data(template_file): """ diff --git a/samcli/commands/local/lib/debug_context.py b/samcli/commands/local/lib/debug_context.py index a7c435f23967..27056077dde3 100644 --- a/samcli/commands/local/lib/debug_context.py +++ b/samcli/commands/local/lib/debug_context.py @@ -1,7 +1,6 @@ """ Information and debug options for a specific runtime. """ -import os class DebugContext(object): @@ -14,8 +13,6 @@ def __init__(self, self.debug_port = debug_port self.debugger_path = debugger_path self.debug_args = debug_args - if self.debug_port: - os.environ["PYTHONUNBUFFERED"] = "1" def __bool__(self): return bool(self.debug_port) diff --git a/samcli/commands/local/lib/local_lambda.py b/samcli/commands/local/lib/local_lambda.py index 6a3e00f4c789..06aef3b91f26 100644 --- a/samcli/commands/local/lib/local_lambda.py +++ b/samcli/commands/local/lib/local_lambda.py @@ -53,11 +53,21 @@ def invoke(self, function_name, event, stdout=None, stderr=None): This function will block until either the function completes or times out. - :param string function_name: Name of the Lambda function to invoke - :param string event: Event data passed to the function. Must be a valid JSON String. - :param io.BaseIO stdout: Stream to write the output of the Lambda function to. - :param io.BaseIO stderr: Stream to write the Lambda runtime logs to. - :raises FunctionNotfound: When we cannot find a function with the given name + Parameters + ---------- + function_name str + Name of the Lambda function to invoke + event str + Event data passed to the function. Must be a valid JSON String. + stdout samcli.lib.utils.stream_writer.StreamWriter + Stream writer to write the output of the Lambda function to. + stderr samcli.lib.utils.stream_writer.StreamWriter + Stream writer to write the Lambda runtime logs to. + + Raises + ------ + FunctionNotfound + When we cannot find a function with the given name """ # Generate the correct configuration based on given inputs diff --git a/samcli/lib/utils/stream_writer.py b/samcli/lib/utils/stream_writer.py new file mode 100644 index 000000000000..223e4d90b8ea --- /dev/null +++ b/samcli/lib/utils/stream_writer.py @@ -0,0 +1,37 @@ +""" +This class acts like a wrapper around output streams to provide any flexibility with output we need +""" + + +class StreamWriter(object): + + def __init__(self, stream, auto_flush=False): + """ + Instatiates new StreamWriter to the specified stream + + Parameters + ---------- + stream io.RawIOBase + Stream to wrap + auto_flush bool + Whether to autoflush the stream upon writing + """ + self._stream = stream + self._auto_flush = auto_flush + + def write(self, output): + """ + Writes specified text to the underlying stream + + Parameters + ---------- + output bytes-like object + Bytes to write + """ + self._stream.write(output) + + if self._auto_flush: + self._stream.flush() + + def flush(self): + self._stream.flush() diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index 609608523110..fb71e0ed6a01 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -7,6 +7,7 @@ from flask import Flask, request from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser, CaseInsensitiveDict +from samcli.lib.utils.stream_writer import StreamWriter from samcli.local.lambdafn.exceptions import FunctionNotFound from samcli.local.events.api_event import ContextIdentity, RequestContext, ApiGatewayLambdaEvent from .service_error_responses import ServiceErrorResponses @@ -40,16 +41,22 @@ def __init__(self, routing_list, lambda_runner, static_dir=None, port=None, host """ Creates an ApiGatewayService - :param list(ApiGatewayCallModel) routing_list: A list of the Model that represent - the service paths to create. - :param samcli.commands.local.lib.local_lambda.LocalLambdaRunner lambda_runner: The Lambda runner class capable - of invoking the function - :param str static_dir: Directory from which to serve static files - :param int port: Optional. port for the service to start listening on - Defaults to 3000 - :param str host: Optional. host to start the service on - Defaults to '127.0.0.1 - :param io.BaseIO stderr: Optional stream where the stderr from Docker container should be written to + Parameters + ---------- + routing_list list(ApiGatewayCallModel) + A list of the Model that represent the service paths to create. + lambda_runner samcli.commands.local.lib.local_lambda.LocalLambdaRunner + The Lambda runner class capable of invoking the function + static_dir str + Directory from which to serve static files + port int + Optional. port for the service to start listening on + Defaults to 3000 + host str + Optional. host to start the service on + Defaults to '127.0.0.1 + stderr samcli.lib.utils.stream_writer.StreamWriter + Optional stream writer where the stderr from Docker container should be written to """ super(LocalApigwService, self).__init__(lambda_runner.is_debugging(), port=port, host=host) self.routing_list = routing_list @@ -123,9 +130,14 @@ def _request_handler(self, **kwargs): * We then transform the response or errors we get from the Invoke and return the data back to the caller - :param kwargs dict: Keyword Args that are passed to the function from Flask. This happens when we have - Path Parameters. - :return: Response object + Parameters + ---------- + kwargs dict + Keyword Args that are passed to the function from Flask. This happens when we have path parameters + + Returns + ------- + Response object """ route = self._get_current_route(request) @@ -135,9 +147,10 @@ def _request_handler(self, **kwargs): return ServiceErrorResponses.lambda_failure_response() stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream, self.is_debugging) try: - self.lambda_runner.invoke(route.function_name, event, stdout=stdout_stream, stderr=self.stderr) + self.lambda_runner.invoke(route.function_name, event, stdout=stdout_stream_writer, stderr=self.stderr) except FunctionNotFound: return ServiceErrorResponses.lambda_not_found_response() diff --git a/samcli/local/docker/container.py b/samcli/local/docker/container.py index 466a139d92f0..41121b7c2217 100644 --- a/samcli/local/docker/container.py +++ b/samcli/local/docker/container.py @@ -168,9 +168,10 @@ def start(self, input_data=None): It waits for the container to complete, fetches both stdout and stderr logs and returns through the given streams. - :param input_data: Optional. Input data sent to the container through container's stdin. - :param io.StringIO stdout: Optional. IO Stream to that receives stdout text from container. - :param io.StringIO stderr: Optional. IO Stream that receives stderr text from container + Parameters + ---------- + input_data + Optional. Input data sent to the container through container's stdin. """ if input_data: @@ -233,10 +234,10 @@ def _write_container_output(output_itr, stdout=None, stderr=None): ---------- output_itr: Iterator Iterator returned by the Docker Attach command - stdout: io.BaseIO, optional - Stream to write stdout data from Container into - stderr: io.BaseIO, optional - Stream to write stderr data from the Container into + stdout: samcli.lib.utils.stream_writer.StreamWriter, optional + Stream writer to write stdout data from Container into + stderr: samcli.lib.utils.stream_writer.StreamWriter, optional + Stream writer to write stderr data from the Container into """ # Iterator returns a tuple of (frame_type, data) where the frame type determines which stream we write output diff --git a/samcli/local/docker/manager.py b/samcli/local/docker/manager.py index 5d2fcd7f3c14..2080acbb0844 100644 --- a/samcli/local/docker/manager.py +++ b/samcli/local/docker/manager.py @@ -3,11 +3,13 @@ """ import logging -import sys +import sys import docker import requests +from samcli.lib.utils.stream_writer import StreamWriter + LOG = logging.getLogger(__name__) @@ -107,11 +109,20 @@ def pull_image(self, image_name, stream=None): """ Ask Docker to pull the container image with given name. - :param string image_name: Name of the image - :param stream: Optional stream to write output to. Defaults to stderr - :raises DockerImagePullFailedException: If the Docker image was not available in the server + Parameters + ---------- + image_name str + Name of the image + stream samcli.lib.utils.stream_writer.StreamWriter + Optional stream writer to output to. Defaults to stderr + + Raises + ------ + DockerImagePullFailedException + If the Docker image was not available in the server """ - stream = stream or sys.stderr + stream_writer = stream or StreamWriter(sys.stderr) + try: result_itr = self.docker_client.api.pull(image_name, stream=True, decode=True) except docker.errors.APIError as ex: @@ -119,16 +130,16 @@ def pull_image(self, image_name, stream=None): raise DockerImagePullFailedException(str(ex)) # io streams, especially StringIO, work only with unicode strings - stream.write(u"\nFetching {} Docker container image...".format(image_name)) + stream_writer.write(u"\nFetching {} Docker container image...".format(image_name)) # Each line contains information on progress of the pull. Each line is a JSON string for _ in result_itr: # For every line, print a dot to show progress - stream.write(u'.') - stream.flush() + stream_writer.write(u'.') + stream_writer.flush() # We are done. Go to the next line - stream.write(u"\n") + stream_writer.write(u"\n") def has_image(self, image_name): """ diff --git a/samcli/local/lambda_service/local_lambda_invoke_service.py b/samcli/local/lambda_service/local_lambda_invoke_service.py index b080e181b3b0..7c684f91595e 100644 --- a/samcli/local/lambda_service/local_lambda_invoke_service.py +++ b/samcli/local/lambda_service/local_lambda_invoke_service.py @@ -6,7 +6,7 @@ from flask import Flask, request - +from samcli.lib.utils.stream_writer import StreamWriter from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser, CaseInsensitiveDict from samcli.local.lambdafn.exceptions import FunctionNotFound from .lambda_error_responses import LambdaErrorResponses @@ -139,9 +139,10 @@ def _invoke_request_handler(self, function_name): request_data = request_data.decode('utf-8') stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream, self.is_debugging) try: - self.lambda_runner.invoke(function_name, request_data, stdout=stdout_stream, stderr=self.stderr) + self.lambda_runner.invoke(function_name, request_data, stdout=stdout_stream_writer, stderr=self.stderr) except FunctionNotFound: LOG.debug('%s was not found to invoke.', function_name) return LambdaErrorResponses.resource_not_found(function_name) diff --git a/tests/functional/commands/local/lib/test_local_lambda.py b/tests/functional/commands/local/lib/test_local_lambda.py index 4a2cf52904b3..bf68b3c7eab6 100644 --- a/tests/functional/commands/local/lib/test_local_lambda.py +++ b/tests/functional/commands/local/lib/test_local_lambda.py @@ -8,6 +8,7 @@ import shutil import logging +from samcli.lib.utils.stream_writer import StreamWriter from samcli.commands.local.lib import provider from samcli.commands.local.lib.local_lambda import LocalLambdaRunner from samcli.local.lambdafn.runtime import LambdaRuntime @@ -82,7 +83,10 @@ def test_must_invoke(self): stdout_stream = io.BytesIO() stderr_stream = io.BytesIO() - runner.invoke(self.function_name, input_event, stdout=stdout_stream, stderr=stderr_stream) + + stdout_stream_writer = StreamWriter(stdout_stream) + stderr_stream_writer = StreamWriter(stderr_stream) + runner.invoke(self.function_name, input_event, stdout=stdout_stream_writer, stderr=stderr_stream_writer) # stderr is where the Lambda container runtime logs are available. It usually contains requestId, start time # etc. So it is non-zero in size @@ -93,4 +97,4 @@ def test_must_invoke(self): for key, value in expected_env_vars.items(): self.assertTrue(key in actual_output, "Key '{}' must be in function output".format(key)) - self.assertEquals(actual_output.get(key), value) + self.assertEqual(actual_output.get(key), value) diff --git a/tests/functional/local/docker/test_lambda_container.py b/tests/functional/local/docker/test_lambda_container.py index 2e42253f9844..28813f6e8b8f 100644 --- a/tests/functional/local/docker/test_lambda_container.py +++ b/tests/functional/local/docker/test_lambda_container.py @@ -10,6 +10,7 @@ from contextlib import contextmanager from unittest import TestCase +from samcli.lib.utils.stream_writer import StreamWriter from samcli.commands.local.lib.debug_context import DebugContext from tests.functional.function_code import nodejs_lambda from samcli.local.docker.lambda_container import LambdaContainer @@ -130,13 +131,17 @@ def test_function_result_is_available_in_stdout_and_logs_in_stderr(self): layer_downloader = LayerDownloader("./", "./") image_builder = LambdaImage(layer_downloader, False, False) container = LambdaContainer(self.runtime, self.handler, self.code_dir, self.layers, image_builder) + stdout_stream = io.BytesIO() stderr_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream) + stderr_stream_writer = StreamWriter(stderr_stream) + with self._create(container): container.start() - container.wait_for_logs(stdout=stdout_stream, stderr=stderr_stream) + container.wait_for_logs(stdout=stdout_stream_writer, stderr=stderr_stream_writer) function_output = stdout_stream.getvalue() function_stderr = stderr_stream.getvalue() diff --git a/tests/functional/local/lambdafn/test_runtime.py b/tests/functional/local/lambdafn/test_runtime.py index 17f825a1948f..ae767cd27c6e 100644 --- a/tests/functional/local/lambdafn/test_runtime.py +++ b/tests/functional/local/lambdafn/test_runtime.py @@ -11,6 +11,7 @@ from parameterized import parameterized, param from tests.functional.function_code import nodejs_lambda, make_zip, ECHO_CODE, SLEEP_CODE, GET_ENV_VAR +from samcli.lib.utils.stream_writer import StreamWriter from samcli.local.docker.manager import ContainerManager from samcli.local.lambdafn.runtime import LambdaRuntime from samcli.local.lambdafn.config import FunctionConfig @@ -59,7 +60,9 @@ def test_echo_function(self): timeout=timeout) stdout_stream = io.BytesIO() - self.runtime.invoke(config, input_event, stdout=stdout_stream) + stdout_stream_writer = StreamWriter(stdout_stream) + + self.runtime.invoke(config, input_event, stdout=stdout_stream_writer) actual_output = stdout_stream.getvalue() self.assertEquals(actual_output.strip(), expected_output) @@ -69,6 +72,8 @@ def test_function_timeout(self): Setup a short timeout and verify that the container is stopped """ stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream) + timeout = 1 # 1 second timeout sleep_seconds = 20 # Ask the function to sleep for 20 seconds @@ -81,7 +86,7 @@ def test_function_timeout(self): # Measure the actual duration of execution start = timer() - self.runtime.invoke(config, str(sleep_seconds), stdout=stdout_stream) + self.runtime.invoke(config, str(sleep_seconds), stdout=stdout_stream_writer) end = timer() # Make sure that the wall clock duration is around the ballpark of timeout value @@ -118,7 +123,9 @@ def test_echo_function_with_zip_file(self, file_name_extension): timeout=timeout) stdout_stream = io.BytesIO() - self.runtime.invoke(config, input_event, stdout=stdout_stream) + stdout_stream_writer = StreamWriter(stdout_stream) + + self.runtime.invoke(config, input_event, stdout=stdout_stream_writer) actual_output = stdout_stream.getvalue() self.assertEquals(actual_output.strip(), expected_output) @@ -129,7 +136,10 @@ def test_check_environment_variables(self): timeout = 30 input_event = "" + stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream) + expected_output = { "AWS_SAM_LOCAL": "true", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", @@ -159,7 +169,7 @@ def test_check_environment_variables(self): config.env_vars.variables = variables config.env_vars.aws_creds = aws_creds - self.runtime.invoke(config, input_event, stdout=stdout_stream) + self.runtime.invoke(config, input_event, stdout=stdout_stream_writer) actual_output = json.loads(stdout_stream.getvalue().strip().decode('utf-8')) # Output is a JSON String. Deserialize. @@ -202,6 +212,8 @@ def _invoke_sleep(self, timeout, sleep_duration, check_stdout, exceptions=None): print("Invoking function " + name) try: stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream) + config = FunctionConfig(name=name, runtime=RUNTIME, handler=HANDLER, @@ -210,7 +222,7 @@ def _invoke_sleep(self, timeout, sleep_duration, check_stdout, exceptions=None): memory=1024, timeout=timeout) - self.runtime.invoke(config, sleep_duration, stdout=stdout_stream) + self.runtime.invoke(config, sleep_duration, stdout=stdout_stream_writer) actual_output = stdout_stream.getvalue().strip() # Must output the sleep duration if check_stdout: self.assertEquals(actual_output.decode('utf-8'), str(sleep_duration)) diff --git a/tests/unit/commands/local/cli_common/test_invoke_context.py b/tests/unit/commands/local/cli_common/test_invoke_context.py index d7ac2b27ed76..597f7a41303a 100644 --- a/tests/unit/commands/local/cli_common/test_invoke_context.py +++ b/tests/unit/commands/local/cli_common/test_invoke_context.py @@ -3,7 +3,6 @@ """ import errno import os -import sys from samcli.commands.local.cli_common.user_exceptions import InvokeContextException, DebugContextException from samcli.commands.local.cli_common.invoke_context import InvokeContext @@ -262,40 +261,210 @@ def test_must_create_runner(self, class TestInvokeContext_stdout_property(TestCase): - def test_must_return_log_file_handle(self): + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stdout") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_enable_auto_flush_if_debug(self, SamFunctionProviderMock, StreamWriterMock, + osutils_stdout_mock, ExitMock): + + context = InvokeContext(template_file="template", debug_port=6000) + + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock() + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + context.stdout + + StreamWriterMock.assert_called_once_with(ANY, True) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stdout") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_not_enable_auto_flush_if_not_debug(self, + SamFunctionProviderMock, StreamWriterMock, + osutils_stdout_mock, ExitMock): + context = InvokeContext(template_file="template") - context._log_file_handle = "handle" - self.assertEquals("handle", context.stdout) + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock() + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + context.stdout + + StreamWriterMock.assert_called_once_with(ANY, False) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stdout") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_use_stdout_if_no_log_file_handle(self, + SamFunctionProviderMock, StreamWriterMock, + osutils_stdout_mock, ExitMock): + + stream_writer_mock = Mock() + StreamWriterMock.return_value = stream_writer_mock + + stdout_mock = Mock() + osutils_stdout_mock.return_value = stdout_mock - def test_must_return_sys_stdout(self): context = InvokeContext(template_file="template") - expected_stdout = sys.stdout + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock(return_value=None) + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + stdout = context.stdout + + StreamWriterMock.assert_called_once_with(stdout_mock, ANY) + self.assertEqual(stream_writer_mock, stdout) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_use_log_file_handle(self, StreamWriterMock, SamFunctionProviderMock, ExitMock): + + stream_writer_mock = Mock() + StreamWriterMock.return_value = stream_writer_mock + + context = InvokeContext(template_file="template") + + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + + log_file_handle_mock = Mock() + context._setup_log_file = Mock(return_value=log_file_handle_mock) + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) - if sys.version_info.major > 2: - expected_stdout = sys.stdout.buffer + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + stdout = context.stdout - self.assertEquals(expected_stdout, context.stdout) + StreamWriterMock.assert_called_once_with(log_file_handle_mock, ANY) + self.assertEqual(stream_writer_mock, stdout) class TestInvokeContext_stderr_property(TestCase): - def test_must_return_log_file_handle(self): + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stderr") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_enable_auto_flush_if_debug(self, SamFunctionProviderMock, StreamWriterMock, + osutils_stderr_mock, ExitMock): + + context = InvokeContext(template_file="template", debug_port=6000) + + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock() + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + context.stderr + + StreamWriterMock.assert_called_once_with(ANY, True) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stderr") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_not_enable_auto_flush_if_not_debug(self, + SamFunctionProviderMock, StreamWriterMock, + osutils_stderr_mock, ExitMock): + context = InvokeContext(template_file="template") - context._log_file_handle = "handle" - self.assertEquals("handle", context.stderr) + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock() + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + context.stderr + + StreamWriterMock.assert_called_once_with(ANY, False) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stderr") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_use_stderr_if_no_log_file_handle(self, + SamFunctionProviderMock, StreamWriterMock, + osutils_stderr_mock, ExitMock): + + stream_writer_mock = Mock() + StreamWriterMock.return_value = stream_writer_mock + + stderr_mock = Mock() + osutils_stderr_mock.return_value = stderr_mock - def test_must_return_sys_stderr(self): context = InvokeContext(template_file="template") - expected_stderr = sys.stderr + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock(return_value=None) + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + stderr = context.stderr + + StreamWriterMock.assert_called_once_with(stderr_mock, ANY) + self.assertEqual(stream_writer_mock, stderr) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_use_log_file_handle(self, StreamWriterMock, SamFunctionProviderMock, ExitMock): + + stream_writer_mock = Mock() + StreamWriterMock.return_value = stream_writer_mock + + context = InvokeContext(template_file="template") + + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + + log_file_handle_mock = Mock() + context._setup_log_file = Mock(return_value=log_file_handle_mock) + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) - if sys.version_info.major > 2: - expected_stderr = sys.stderr.buffer + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + stderr = context.stderr - self.assertEquals(expected_stderr, context.stderr) + StreamWriterMock.assert_called_once_with(log_file_handle_mock, ANY) + self.assertEqual(stream_writer_mock, stderr) class TestInvokeContext_template_property(TestCase): diff --git a/tests/unit/commands/local/lib/test_debug_context.py b/tests/unit/commands/local/lib/test_debug_context.py index c7f38f4f91cf..6d8f0a347a14 100644 --- a/tests/unit/commands/local/lib/test_debug_context.py +++ b/tests/unit/commands/local/lib/test_debug_context.py @@ -1,4 +1,3 @@ -import os from unittest import TestCase from parameterized import parameterized @@ -29,7 +28,6 @@ def test_bool_truthy(self, port, debug_path, debug_ars): debug_context = DebugContext(port, debug_path, debug_ars) self.assertTrue(debug_context.__bool__()) - self.assertTrue(os.environ["PYTHONUNBUFFERED"], "1") @parameterized.expand([ (None, 'debuggerpath', 'debug_args'), @@ -41,7 +39,6 @@ def test_bool_falsy(self, port, debug_path, debug_ars): debug_context = DebugContext(port, debug_path, debug_ars) self.assertFalse(debug_context.__bool__()) - self.assertFalse(False, "PYTHONUNBUFFERED" in os.environ.keys()) @parameterized.expand([ ('1000', 'debuggerpath', 'debug_args'), diff --git a/tests/unit/lib/utils/test_stream_writer.py b/tests/unit/lib/utils/test_stream_writer.py new file mode 100644 index 000000000000..67a57b0996ef --- /dev/null +++ b/tests/unit/lib/utils/test_stream_writer.py @@ -0,0 +1,52 @@ +""" +Tests for StreamWriter +""" + +from unittest import TestCase + +from samcli.lib.utils.stream_writer import StreamWriter + +from mock import Mock + + +class TestStreamWriter(TestCase): + + def test_must_write_to_stream(self): + buffer = "something" + stream_mock = Mock() + + writer = StreamWriter(stream_mock) + writer.write(buffer) + + stream_mock.write.assert_called_once_with(buffer) + + def test_must_flush_underlying_stream(self): + stream_mock = Mock() + writer = StreamWriter(stream_mock) + + writer.flush() + + stream_mock.flush.assert_called_once_with() + + def test_auto_flush_must_be_off_by_default(self): + stream_mock = Mock() + + writer = StreamWriter(stream_mock) + writer.write("something") + + stream_mock.flush.assert_not_called() + + def test_when_auto_flush_on_flush_after_each_write(self): + stream_mock = Mock() + flush_mock = Mock() + + stream_mock.flush = flush_mock + + lines = ["first", "second", "third"] + + writer = StreamWriter(stream_mock, True) + + for line in lines: + writer.write(line) + flush_mock.assert_called_once_with() + flush_mock.reset_mock()