예제 #1
0
class TestLambdaRuntime(TestCase):

    # Approx Number of seconds it takes to startup a Docker container. This helps us measure
    # the approx time that the Lambda Function actually ran for
    CONTAINER_STARTUP_OVERHEAD_SECONDS = 5

    def setUp(self):
        self.code_dir = {
            "echo": nodejs_lambda(ECHO_CODE),
            "sleep": nodejs_lambda(SLEEP_CODE),
            "envvar": nodejs_lambda(GET_ENV_VAR)
        }

        self.container_manager = ContainerManager()
        layer_downloader = LayerDownloader("./", "./")
        self.lambda_image = LambdaImage(layer_downloader, False, False)
        self.runtime = LambdaRuntime(self.container_manager, self.lambda_image)

    def tearDown(self):
        for _, dir in self.code_dir.items():
            shutil.rmtree(dir)

    def test_echo_function(self):
        timeout = 3
        input_event = '{"a":"b"}'
        expected_output = b'{"a":"b"}'

        config = FunctionConfig(name="helloworld",
                                runtime=RUNTIME,
                                handler=HANDLER,
                                code_abs_path=self.code_dir["echo"],
                                layers=[],
                                timeout=timeout)

        stdout_stream = io.BytesIO()
        self.runtime.invoke(config, input_event, stdout=stdout_stream)

        actual_output = stdout_stream.getvalue()
        self.assertEquals(actual_output.strip(), expected_output)

    def test_function_timeout(self):
        """
        Setup a short timeout and verify that the container is stopped
        """
        stdout_stream = io.BytesIO()
        timeout = 1  # 1 second timeout
        sleep_seconds = 20  # Ask the function to sleep for 20 seconds

        config = FunctionConfig(name="sleep_timeout",
                                runtime=RUNTIME,
                                handler=HANDLER,
                                code_abs_path=self.code_dir["sleep"],
                                layers=[],
                                timeout=timeout)

        # Measure the actual duration of execution
        start = timer()
        self.runtime.invoke(config, str(sleep_seconds), stdout=stdout_stream)
        end = timer()

        # Make sure that the wall clock duration is around the ballpark of timeout value
        wall_clock_func_duration = end - start
        print("Function completed in {} seconds".format(
            wall_clock_func_duration))
        # The function should *not* preemptively stop
        self.assertGreater(wall_clock_func_duration, timeout - 1)
        # The function should not run for much longer than timeout.
        self.assertLess(wall_clock_func_duration,
                        timeout + self.CONTAINER_STARTUP_OVERHEAD_SECONDS)

        # There should be no output from the function because timer was interrupted
        actual_output = stdout_stream.getvalue()
        self.assertEquals(actual_output.strip(), b"")

    @parameterized.expand([("zip"), ("jar"), ("ZIP"), ("JAR")])
    def test_echo_function_with_zip_file(self, file_name_extension):
        timeout = 3
        input_event = '"this input should be echoed"'
        expected_output = b'"this input should be echoed"'

        code_dir = self.code_dir["echo"]
        with make_zip(code_dir, file_name_extension) as code_zip_path:

            config = FunctionConfig(name="helloworld",
                                    runtime=RUNTIME,
                                    handler=HANDLER,
                                    code_abs_path=code_zip_path,
                                    layers=[],
                                    timeout=timeout)

            stdout_stream = io.BytesIO()
            self.runtime.invoke(config, input_event, stdout=stdout_stream)

            actual_output = stdout_stream.getvalue()
            self.assertEquals(actual_output.strip(), expected_output)

    def test_check_environment_variables(self):
        variables = {"var1": "value1", "var2": "value2"}
        aws_creds = {
            "region": "ap-south-1",
            "key": "mykey",
            "secret": "mysecret"
        }

        timeout = 30
        input_event = ""
        stdout_stream = io.BytesIO()
        expected_output = {
            "AWS_SAM_LOCAL": "true",
            "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024",
            "AWS_LAMBDA_FUNCTION_TIMEOUT": "30",
            "AWS_LAMBDA_FUNCTION_HANDLER": "index.handler",

            # Values coming from AWS Credentials
            "AWS_REGION": "ap-south-1",
            "AWS_DEFAULT_REGION": "ap-south-1",
            "AWS_ACCESS_KEY_ID": "mykey",
            "AWS_SECRET_ACCESS_KEY": "mysecret",

            # Custom environment variables
            "var1": "value1",
            "var2": "value2"
        }

        config = FunctionConfig(name="helloworld",
                                runtime=RUNTIME,
                                handler=HANDLER,
                                code_abs_path=self.code_dir["envvar"],
                                layers=[],
                                memory=MEMORY,
                                timeout=timeout)

        # Set the appropriate environment variables
        config.env_vars.variables = variables
        config.env_vars.aws_creds = aws_creds

        self.runtime.invoke(config, input_event, stdout=stdout_stream)

        actual_output = json.loads(stdout_stream.getvalue().strip().decode(
            'utf-8'))  # Output is a JSON String. Deserialize.

        # Make sure all key/value from expected_output is present in actual_output
        for key, value in expected_output.items():
            # Do the key check first to print a nice error error message when it fails
            self.assertTrue(
                key in actual_output,
                "'{}' should be in environment variable output".format(key))
            self.assertEquals(
                actual_output[key], expected_output[key],
                "Value of environment variable '{}' differs fromm expectation".
                format(key))
예제 #2
0
class LambdaRuntime_invoke(TestCase):

    DEFAULT_MEMORY = 128
    DEFAULT_TIMEOUT = 3

    def setUp(self):

        self.manager_mock = Mock()

        self.name = "name"
        self.lang = "runtime"
        self.handler = "handler"
        self.code_path = "code-path"
        self.layers = []
        self.func_config = FunctionConfig(self.name, self.lang, self.handler, self.code_path, self.layers)

        self.env_vars = Mock()
        self.func_config.env_vars = self.env_vars
        self.env_var_value = {"a": "b"}
        self.env_vars.resolve.return_value = self.env_var_value

    @patch("samcli.local.lambdafn.runtime.LambdaContainer")
    def test_must_run_container_and_wait_for_logs(self, LambdaContainerMock):
        event = "event"
        code_dir = "some code dir"
        stdout = "stdout"
        stderr = "stderr"
        container = Mock()
        timer = Mock()
        debug_options = Mock()
        lambda_image_mock = Mock()

        self.runtime = LambdaRuntime(self.manager_mock, lambda_image_mock)

        # Using MagicMock to mock the context manager
        self.runtime._get_code_dir = MagicMock()
        self.runtime._get_code_dir(self.code_path).__enter__.return_value = code_dir

        # Configure interrupt handler
        self.runtime._configure_interrupt = Mock()
        self.runtime._configure_interrupt.return_value = timer

        LambdaContainerMock.return_value = container

        self.runtime.invoke(self.func_config,
                            event,
                            debug_context=debug_options,
                            stdout=stdout,
                            stderr=stderr)

        # Verify if Lambda Event data is set
        self.env_vars.add_lambda_event_body.assert_called_with(event)

        # Make sure env-vars get resolved
        self.env_vars.resolve.assert_called_with()

        # Make sure the context manager is called to return the code directory
        self.runtime._get_code_dir.assert_called_with(self.code_path)

        # Make sure the container is created with proper values
        LambdaContainerMock.assert_called_with(self.lang, self.handler, code_dir, self.layers, lambda_image_mock,
                                               memory_mb=self.DEFAULT_MEMORY, env_vars=self.env_var_value,
                                               debug_options=debug_options)

        # Run the container and get results
        self.manager_mock.run.assert_called_with(container)
        self.runtime._configure_interrupt.assert_called_with(self.name, self.DEFAULT_TIMEOUT, container, True)
        container.wait_for_logs.assert_called_with(stdout=stdout, stderr=stderr)

        # Finally block
        timer.cancel.assert_called_with()
        self.manager_mock.stop.assert_called_with(container)

    @patch("samcli.local.lambdafn.runtime.LambdaContainer")
    def test_exception_from_run_must_trigger_cleanup(self, LambdaContainerMock):
        event = "event"
        code_dir = "some code dir"
        stdout = "stdout"
        stderr = "stderr"
        container = Mock()
        timer = Mock()
        layer_downloader = Mock()

        self.runtime = LambdaRuntime(self.manager_mock, layer_downloader)

        # Using MagicMock to mock the context manager
        self.runtime._get_code_dir = MagicMock()
        self.runtime._get_code_dir(self.code_path).__enter__.return_value = code_dir
        self.runtime._configure_interrupt = Mock()
        self.runtime._configure_interrupt.return_value = timer

        LambdaContainerMock.return_value = container

        self.manager_mock.run.side_effect = ValueError("some exception")

        with self.assertRaises(ValueError):
            self.runtime.invoke(self.func_config,
                                event,
                                debug_context=None,
                                stdout=stdout,
                                stderr=stderr)

        # Run the container and get results
        self.manager_mock.run.assert_called_with(container)

        self.runtime._configure_interrupt.assert_not_called()

        # Finally block must be called
        # But timer was not yet created. It should not be called
        timer.cancel.assert_not_called()
        # In any case, stop the container
        self.manager_mock.stop.assert_called_with(container)

    @patch("samcli.local.lambdafn.runtime.LambdaContainer")
    def test_exception_from_wait_for_logs_must_trigger_cleanup(self, LambdaContainerMock):
        event = "event"
        code_dir = "some code dir"
        stdout = "stdout"
        stderr = "stderr"
        container = Mock()
        timer = Mock()
        debug_options = Mock()
        layer_downloader = Mock()

        self.runtime = LambdaRuntime(self.manager_mock, layer_downloader)

        # Using MagicMock to mock the context manager
        self.runtime._get_code_dir = MagicMock()
        self.runtime._get_code_dir(self.code_path).__enter__.return_value = code_dir
        self.runtime._configure_interrupt = Mock()
        self.runtime._configure_interrupt.return_value = timer

        LambdaContainerMock.return_value = container

        container.wait_for_logs.side_effect = ValueError("some exception")

        with self.assertRaises(ValueError):
            self.runtime.invoke(self.func_config,
                                event,
                                debug_context=debug_options,
                                stdout=stdout,
                                stderr=stderr)

        # Run the container and get results
        self.manager_mock.run.assert_called_with(container)

        self.runtime._configure_interrupt.assert_called_with(self.name, self.DEFAULT_TIMEOUT, container, True)

        # Finally block must be called
        # Timer was created. So it must be cancelled
        timer.cancel.assert_called_with()
        # In any case, stop the container
        self.manager_mock.stop.assert_called_with(container)

    @patch("samcli.local.lambdafn.runtime.LambdaContainer")
    def test_keyboard_interrupt_must_not_raise(self, LambdaContainerMock):
        event = "event"
        code_dir = "some code dir"
        stdout = "stdout"
        stderr = "stderr"
        container = Mock()
        layer_downloader = Mock()

        self.runtime = LambdaRuntime(self.manager_mock, layer_downloader)

        # Using MagicMock to mock the context manager
        self.runtime._get_code_dir = MagicMock()
        self.runtime._get_code_dir(self.code_path).__enter__.return_value = code_dir
        self.runtime._configure_interrupt = Mock()

        LambdaContainerMock.return_value = container

        self.manager_mock.run.side_effect = KeyboardInterrupt("some exception")

        self.runtime.invoke(self.func_config,
                            event,
                            stdout=stdout,
                            stderr=stderr)

        # Run the container and get results
        self.manager_mock.run.assert_called_with(container)

        self.runtime._configure_interrupt.assert_not_called()

        # Finally block must be called
        self.manager_mock.stop.assert_called_with(container)
예제 #3
0
class TestLambdaRuntime_MultipleInvokes(TestCase):
    def setUp(self):
        self.code_dir = nodejs_lambda(SLEEP_CODE)

        Input = namedtuple('Input', ["timeout", "sleep", "check_stdout"])
        self.inputs = [
            Input(sleep=1, timeout=10, check_stdout=True),
            Input(sleep=2, timeout=10, check_stdout=True),
            Input(sleep=3, timeout=10, check_stdout=True),
            Input(sleep=5, timeout=10, check_stdout=True),
            Input(sleep=8, timeout=10, check_stdout=True),
            Input(sleep=13, timeout=12, check_stdout=False),  # Must timeout
            Input(sleep=21, timeout=20,
                  check_stdout=False),  # Must timeout. So stdout will be empty
        ]
        random.shuffle(self.inputs)

        container_manager = ContainerManager()
        layer_downloader = LayerDownloader("./", "./")
        self.lambda_image = LambdaImage(layer_downloader, False, False)
        self.runtime = LambdaRuntime(container_manager, self.lambda_image)

    def tearDown(self):
        shutil.rmtree(self.code_dir)

    def _invoke_sleep(self,
                      timeout,
                      sleep_duration,
                      check_stdout,
                      exceptions=None):

        name = "sleepfunction_timeout_{}_sleep_{}".format(
            timeout, sleep_duration)
        print("Invoking function " + name)
        try:
            stdout_stream = io.BytesIO()
            config = FunctionConfig(name=name,
                                    runtime=RUNTIME,
                                    handler=HANDLER,
                                    code_abs_path=self.code_dir,
                                    layers=[],
                                    memory=1024,
                                    timeout=timeout)

            self.runtime.invoke(config, sleep_duration, stdout=stdout_stream)
            actual_output = stdout_stream.getvalue().strip(
            )  # Must output the sleep duration
            if check_stdout:
                self.assertEquals(actual_output.decode('utf-8'),
                                  str(sleep_duration))
        except Exception as ex:
            if exceptions is not None:
                exceptions.append({"name": name, "error": ex})
            else:
                raise

    def test_serial(self):
        """
        Making sure we can invoke multiple times on the same ``LambdaRuntime`` object. This is test is necessary to
        catch timer that was not cancelled, race conditions, memory leak issues, etc.
        """

        for input in self.inputs:
            self._invoke_sleep(input.timeout, input.sleep, input.check_stdout)

    def test_parallel(self):
        """
        Making sure we can invoke multiple times on the same ``LambdaRuntime`` object. This is test is necessary to
        catch timer that was not cancelled, race conditions, memory leak issues, etc.
        """

        threads = []

        # Collect all exceptions from threads. This is important because exceptions reported in thread don't bubble
        # to the main thread. Therefore test runner will never catch and fail the test.
        exceptions = []

        for input in self.inputs:

            t = threading.Thread(name='thread',
                                 target=self._invoke_sleep,
                                 args=(input.timeout, input.sleep,
                                       input.check_stdout, exceptions))
            t.setDaemon(True)
            t.start()
            threads.append(t)

        # Wait for all threads to exit
        for t in threads:
            t.join()

        for e in exceptions:
            print("-------------")
            print("ERROR in function " + e["name"])
            print(e["error"])
            print("-------------")

        if len(exceptions) > 0:
            raise AssertionError(
                "Test failed. See print outputs above for details on the thread that failed"
            )